Fix bugs in CI workflow
Some checks failed
CI/CD Pipeline with Secure Ephemeral PiP / test-backend (push) Failing after 33s
CI/CD Pipeline with Secure Ephemeral PiP / test-frontend (push) Has been skipped
CI/CD Pipeline with Secure Ephemeral PiP / build-backend (push) Has been skipped
CI/CD Pipeline with Secure Ephemeral PiP / build-frontend (push) Has been skipped
CI/CD Pipeline with Secure Ephemeral PiP / deploy-prod (push) Has been skipped

This commit is contained in:
continuist 2025-09-09 23:37:25 -04:00
parent f3f80f2679
commit ce66586ec0
2 changed files with 137 additions and 40 deletions

View file

@ -31,6 +31,18 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Resolve Podman user socket for this runner
run: |
SVC_USER="ci-service" # CI runner user on the CI host
uid=$(id -u "$SVC_USER")
echo "PODMAN_SOCK=/run/user/${uid}/podman/podman.sock" >> "$GITHUB_ENV"
- name: Verify runner wiring to Podman
run: |
podman --version
test -S "${PODMAN_SOCK}" || { echo "Missing socket ${PODMAN_SOCK}"; exit 1; }
# Optional: sanity poke of the service via PiP later
- name: Network/DNS sanity from job container - name: Network/DNS sanity from job container
run: | run: |
getent hosts git.gcdo.org || true getent hosts git.gcdo.org || true
@ -46,6 +58,7 @@ jobs:
- name: Setup ephemeral PiP container - name: Setup ephemeral PiP container
env: env:
PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }} PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }}
SOCKET_PATH: ${{ env.PODMAN_SOCK }}
run: | run: |
chmod +x ./secure_pip_setup.sh chmod +x ./secure_pip_setup.sh
./secure_pip_setup.sh ./secure_pip_setup.sh
@ -57,6 +70,9 @@ jobs:
chmod +x ./pip_ready.sh chmod +x ./pip_ready.sh
./pip_ready.sh ./pip_ready.sh
- name: Network sanity from PiP
run: podman exec ci-pip-${{ env.RUN_ID }} sh -lc 'getent hosts git.gcdo.org && curl -sS -o /dev/null -w "status=%{http_code}\n" https://git.gcdo.org/api/healthz'
- name: Configure Git for token-based authentication - name: Configure Git for token-based authentication
run: | run: |
git config --global url."https://${{ secrets.REGISTRY_USERNAME }}:${{ secrets.REGISTRY_TOKEN }}@${{ env.REGISTRY }}".insteadOf "https://${{ env.REGISTRY }}" git config --global url."https://${{ secrets.REGISTRY_USERNAME }}:${{ secrets.REGISTRY_TOKEN }}@${{ env.REGISTRY }}".insteadOf "https://${{ env.REGISTRY }}"
@ -85,7 +101,7 @@ jobs:
env: env:
WORKSPACE: ${{ github.workspace }} WORKSPACE: ${{ github.workspace }}
run: | run: |
podman exec ci-pip-${{ env.RUN_ID }} sh -lc ' podman exec -e WORKSPACE="${WORKSPACE}" ci-pip-${{ env.RUN_ID }} sh -lc '
podman run --rm \ podman run --rm \
-v "$WORKSPACE":/workspace \ -v "$WORKSPACE":/workspace \
-w /workspace \ -w /workspace \
@ -95,11 +111,10 @@ jobs:
- name: Run backend integration tests - name: Run backend integration tests
env: env:
WORKSPACE: ${{ github.workspace }} WORKSPACE: ${{ github.workspace }}
RUN_ID: ${{ env.RUN_ID }}
run: | run: |
podman exec ci-pip-${{ env.RUN_ID }} sh -lc ' podman exec -e WORKSPACE="${WORKSPACE}" ci-pip-${{ env.RUN_ID }} sh -lc '
podman run --rm \ podman run --rm \
--network integ-'"$RUN_ID"' \ --network integ-${{ env.RUN_ID }} \
-v "$WORKSPACE":/workspace \ -v "$WORKSPACE":/workspace \
-w /workspace \ -w /workspace \
-e DATABASE_URL=postgres://postgres:password@test-postgres:5432/sharenet_test \ -e DATABASE_URL=postgres://postgres:password@test-postgres:5432/sharenet_test \
@ -122,6 +137,18 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Resolve Podman user socket for this runner
run: |
SVC_USER="ci-service" # CI runner user on the CI host
uid=$(id -u "$SVC_USER")
echo "PODMAN_SOCK=/run/user/${uid}/podman/podman.sock" >> "$GITHUB_ENV"
- name: Verify runner wiring to Podman
run: |
podman --version
test -S "${PODMAN_SOCK}" || { echo "Missing socket ${PODMAN_SOCK}"; exit 1; }
# Optional: sanity poke of the service via PiP later
- name: Verify pinned digests provided - name: Verify pinned digests provided
run: | run: |
for v in NODE_IMG_DIGEST PODMAN_CLIENT_IMG_DIGEST; do for v in NODE_IMG_DIGEST PODMAN_CLIENT_IMG_DIGEST; do
@ -132,6 +159,7 @@ jobs:
- name: Setup ephemeral PiP container - name: Setup ephemeral PiP container
env: env:
PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }} PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }}
SOCKET_PATH: ${{ env.PODMAN_SOCK }}
run: | run: |
chmod +x ./secure_pip_setup.sh chmod +x ./secure_pip_setup.sh
./secure_pip_setup.sh ./secure_pip_setup.sh
@ -143,11 +171,14 @@ jobs:
chmod +x ./pip_ready.sh chmod +x ./pip_ready.sh
./pip_ready.sh ./pip_ready.sh
- name: Network sanity from PiP
run: podman exec ci-pip-${{ env.RUN_ID }} sh -lc 'getent hosts git.gcdo.org && curl -sS -o /dev/null -w "status=%{http_code}\n" https://git.gcdo.org/api/healthz'
- name: Run frontend tests (digest-pinned) - name: Run frontend tests (digest-pinned)
env: env:
WORKSPACE: ${{ github.workspace }} WORKSPACE: ${{ github.workspace }}
run: | run: |
podman exec ci-pip-${{ env.RUN_ID }} sh -lc ' podman exec -e WORKSPACE="${WORKSPACE}" ci-pip-${{ env.RUN_ID }} sh -lc '
podman run --rm \ podman run --rm \
-v "$WORKSPACE":/workspace \ -v "$WORKSPACE":/workspace \
-w /workspace \ -w /workspace \
@ -164,9 +195,22 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Resolve Podman user socket for this runner
run: |
SVC_USER="ci-service" # CI runner user on the CI host
uid=$(id -u "$SVC_USER")
echo "PODMAN_SOCK=/run/user/${uid}/podman/podman.sock" >> "$GITHUB_ENV"
- name: Verify runner wiring to Podman
run: |
podman --version
test -S "${PODMAN_SOCK}" || { echo "Missing socket ${PODMAN_SOCK}"; exit 1; }
# Optional: sanity poke of the service via PiP later
- name: Setup ephemeral PiP container - name: Setup ephemeral PiP container
env: env:
PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }} PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }}
SOCKET_PATH: ${{ env.PODMAN_SOCK }}
run: | run: |
chmod +x ./secure_pip_setup.sh chmod +x ./secure_pip_setup.sh
./secure_pip_setup.sh ./secure_pip_setup.sh
@ -178,6 +222,9 @@ jobs:
chmod +x ./pip_ready.sh chmod +x ./pip_ready.sh
./pip_ready.sh ./pip_ready.sh
- name: Network sanity from PiP
run: podman exec ci-pip-${{ env.RUN_ID }} sh -lc 'getent hosts git.gcdo.org && curl -sS -o /dev/null -w "status=%{http_code}\n" https://git.gcdo.org/api/healthz'
- name: Login to Forgejo Container Registry securely - name: Login to Forgejo Container Registry securely
env: env:
REGISTRY_HOST: ${{ env.REGISTRY }} REGISTRY_HOST: ${{ env.REGISTRY }}
@ -215,9 +262,22 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Resolve Podman user socket for this runner
run: |
SVC_USER="ci-service" # CI runner user on the CI host
uid=$(id -u "$SVC_USER")
echo "PODMAN_SOCK=/run/user/${uid}/podman/podman.sock" >> "$GITHUB_ENV"
- name: Verify runner wiring to Podman
run: |
podman --version
test -S "${PODMAN_SOCK}" || { echo "Missing socket ${PODMAN_SOCK}"; exit 1; }
# Optional: sanity poke of the service via PiP later
- name: Setup ephemeral PiP container - name: Setup ephemeral PiP container
env: env:
PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }} PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }}
SOCKET_PATH: ${{ env.PODMAN_SOCK }}
run: | run: |
chmod +x ./secure_pip_setup.sh chmod +x ./secure_pip_setup.sh
./secure_pip_setup.sh ./secure_pip_setup.sh
@ -229,6 +289,9 @@ jobs:
chmod +x ./pip_ready.sh chmod +x ./pip_ready.sh
./pip_ready.sh ./pip_ready.sh
- name: Network sanity from PiP
run: podman exec ci-pip-${{ env.RUN_ID }} sh -lc 'getent hosts git.gcdo.org && curl -sS -o /dev/null -w "status=%{http_code}\n" https://git.gcdo.org/api/healthz'
- name: Login to Forgejo Container Registry securely - name: Login to Forgejo Container Registry securely
env: env:
REGISTRY_HOST: ${{ env.REGISTRY }} REGISTRY_HOST: ${{ env.REGISTRY }}
@ -292,11 +355,31 @@ jobs:
fi fi
} }
# Substitute environment variables in prod-pod.yml - name: Resolve Podman user socket (prod)
envsubst < deploy/prod-pod.yml > prod-pod-deploy.yml run: |
SVC_USER="prod-service"
uid=$(id -u "$SVC_USER")
echo "PODMAN_SOCK=/run/user/${uid}/podman/podman.sock" >> "$GITHUB_ENV"
- name: Verify runner wiring to Podman
run: |
podman --version
test -S "${PODMAN_SOCK}" || { echo "Missing socket ${PODMAN_SOCK}"; exit 1; }
# Optional: sanity poke of the service via PiP later
- name: Render prod spec
run: envsubst < deploy/prod-pod.yml > prod-pod-deploy.yml
- name: Apply with Podman (no registry login needed)
env:
PODMAN_CLIENT_IMG_DIGEST: ${{ env.PODMAN_CLIENT_IMG_DIGEST }}
run: |
cat prod-pod-deploy.yml | \
podman run --rm -i \
-v "${PODMAN_SOCK}:${PODMAN_SOCK}" \
-e "CONTAINER_HOST=unix://${PODMAN_SOCK}" \
"${PODMAN_CLIENT_IMG_DIGEST}" \
sh -lc 'cat >/tmp/spec.yml; podman play kube --replace /tmp/spec.yml'
# Deploy the pod (replace existing if it exists) - name: Clean up generated file
podman play kube --replace prod-pod-deploy.yml run: rm -f prod-pod-deploy.yml
# Clean up generated file
rm -f prod-pod-deploy.yml

View file

@ -8,18 +8,15 @@ metadata:
io.containers.no-new-privileges: "true" io.containers.no-new-privileges: "true"
spec: spec:
hostname: sharenet-production-pod hostname: sharenet-production-pod
# Security: run as non-root user with specific UID/GID (matches PROD_SERVICE_USER)
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers: containers:
- name: postgres - name: postgres
image: ${REGISTRY_HOST}/${APP_NAME}/postgres:${IMAGE_TAG} image: ${REGISTRY_HOST}/${APP_NAME}/postgres:${IMAGE_TAG}
# Security: drop all capabilities, read-only root filesystem except data volume # Security: drop all capabilities, read-only root filesystem except data volume
securityContext: securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
@ -29,26 +26,27 @@ spec:
value: ${POSTGRES_DATABASE_NAME} value: ${POSTGRES_DATABASE_NAME}
- name: POSTGRES_USER - name: POSTGRES_USER
value: ${POSTGRES_USERNAME} value: ${POSTGRES_USERNAME}
- name: POSTGRES_PASSWORD
value: ${POSTGRES_PASSWORD}
- name: PGDATA - name: PGDATA
value: /var/lib/postgresql/data/pgdata value: /var/lib/postgresql/data/pgdata
ports: ports:
- containerPort: ${POSTGRES_PORT} - containerPort: ${POSTGRES_PORT}
protocol: TCP protocol: TCP
volumeMounts: volumeMounts:
- name: pgdata - { name: pgdata, mountPath: /var/lib/postgresql/data, readOnly: false }
mountPath: /var/lib/postgresql/data - { name: postgres-run, mountPath: /var/run/postgresql, readOnly: false }
readOnly: false
# Health checks # Health checks
livenessProbe: livenessProbe:
exec: exec:
command: ["pg_isready", "-U", "sharenet"] command: ["pg_isready", "-U", "${POSTGRES_USERNAME}"]
initialDelaySeconds: 30 initialDelaySeconds: 30
periodSeconds: 30 periodSeconds: 30
timeoutSeconds: 10 timeoutSeconds: 10
failureThreshold: 3 failureThreshold: 3
readinessProbe: readinessProbe:
exec: exec:
command: ["pg_isready", "-U", "sharenet"] command: ["pg_isready", "-U", "${POSTGRES_USERNAME}"]
initialDelaySeconds: 5 initialDelaySeconds: 5
periodSeconds: 10 periodSeconds: 10
timeoutSeconds: 5 timeoutSeconds: 5
@ -65,23 +63,29 @@ spec:
image: ${REGISTRY_HOST}/${APP_NAME}/backend:${IMAGE_TAG} image: ${REGISTRY_HOST}/${APP_NAME}/backend:${IMAGE_TAG}
# Security: drop all capabilities, read-only root filesystem # Security: drop all capabilities, read-only root filesystem
securityContext: securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
drop: ["ALL"] drop: ["ALL"]
env: env:
- name: DATABASE_URL - name: DATABASE_URL
value: postgres://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@{POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DATABASE_NAME}?sslmode=disable value: "postgres://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@localhost:${POSTGRES_PORT}/${POSTGRES_DATABASE_NAME}?sslmode=disable"
- name: PORT - name: PORT
value: ${PROD_BACKEND_PORT} value: "${PROD_BACKEND_PORT}"
ports: ports:
- containerPort: ${PROD_BACKEND_PORT} - containerPort: ${PROD_BACKEND_PORT}
protocol: TCP protocol: TCP
volumeMounts:
- { name: backend-tmp, mountPath: /tmp }
- { name: backend-tmp, mountPath: /run }
# Health checks # Health checks
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /health path: /health
port: ${PROD_BACKEND_PORT} port: "${PROD_BACKEND_PORT}"
scheme: HTTP scheme: HTTP
initialDelaySeconds: 30 initialDelaySeconds: 30
periodSeconds: 30 periodSeconds: 30
@ -90,7 +94,7 @@ spec:
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /health path: /health
port: ${PROD_BACKEND_PORT} port: "${PROD_BACKEND_PORT}"
scheme: HTTP scheme: HTTP
initialDelaySeconds: 10 initialDelaySeconds: 10
periodSeconds: 10 periodSeconds: 10
@ -108,6 +112,9 @@ spec:
image: ${REGISTRY_HOST}/${APP_NAME}/frontend:${IMAGE_TAG} image: ${REGISTRY_HOST}/${APP_NAME}/frontend:${IMAGE_TAG}
# Security: drop all capabilities, read-only root filesystem # Security: drop all capabilities, read-only root filesystem
securityContext: securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
@ -116,12 +123,15 @@ spec:
- name: NEXT_PUBLIC_API_HOST - name: NEXT_PUBLIC_API_HOST
value: ${PROD_BACKEND_HOST} value: ${PROD_BACKEND_HOST}
- name: NEXT_PUBLIC_API_PORT - name: NEXT_PUBLIC_API_PORT
value: ${PROD_BACKEND_PORT} value: "${PROD_BACKEND_PORT}"
- name: PORT - name: PORT
value: ${PROD_FRONTEND_PORT} value: "${PROD_FRONTEND_PORT}"
ports: ports:
- containerPort: ${PROD_FRONTEND_PORT} - containerPort: ${PROD_FRONTEND_PORT}
protocol: TCP protocol: TCP
volumeMounts:
- { name: frontend-tmp, mountPath: /tmp }
- { name: frontend-tmp, mountPath: /run }
# Resource limits # Resource limits
resources: resources:
requests: requests:
@ -141,25 +151,21 @@ spec:
drop: ["ALL"] drop: ["ALL"]
ports: ports:
- containerPort: 80 - containerPort: 80
hostPort: 8080
protocol: TCP protocol: TCP
- containerPort: 443 - containerPort: 443
hostPort: 8443
protocol: TCP protocol: TCP
volumeMounts: volumeMounts:
- name: nginx-conf - { name: nginx-run, mountPath: /var/run, readOnly: false }
mountPath: /etc/nginx/nginx.conf - { name: nginx-conf, mountPath: /etc/nginx/nginx.conf, readOnly: true, subPath: nginx.conf }
subPath: nginx.conf - { name: nginx-cache, mountPath: /var/cache/nginx, readOnly: false }
readOnly: true - { name: letsencrypt, mountPath: /etc/letsencrypt, readOnly: true }
- name: nginx-cache
mountPath: /var/cache/nginx
readOnly: false
- name: letsencrypt
mountPath: /etc/letsencrypt
readOnly: true
# Health check # Health check
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /healthz path: /healthz
port: 80 port: 8090
scheme: HTTP scheme: HTTP
initialDelaySeconds: 10 initialDelaySeconds: 10
periodSeconds: 30 periodSeconds: 30
@ -179,6 +185,14 @@ spec:
hostPath: hostPath:
path: /opt/sharenet/volumes/pgdata path: /opt/sharenet/volumes/pgdata
type: DirectoryOrCreate type: DirectoryOrCreate
- name: postgres-run
emptyDir: {}
- name: backend-tmp
emptyDir: { medium: Memory }
- name: frontend-tmp
emptyDir: { medium: Memory }
- name: nginx-run
emptyDir: {}
- name: nginx-conf - name: nginx-conf
hostPath: hostPath:
path: /opt/sharenet/nginx path: /opt/sharenet/nginx