Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c27f6a1e3c | |||
| f283d5aa02 | |||
| 39804135a4 | |||
| b2c4692400 | |||
| a0088acb1a | |||
| eff1098289 | |||
| 8eeaa92ad8 | |||
| fc3a0b4d92 | |||
| 009aa92777 | |||
| 284b361f9b | |||
| 3dcf0ce021 | |||
| b3a452be50 | |||
| 440d7ac7e7 | |||
| 83b553b58e | |||
| 3a69ec29b5 | |||
| 2573de86d5 | |||
| 06162f9f15 | |||
| fb70b816f2 | |||
| d92bcf433b | |||
| 01ed6dac00 | |||
| a7a55bbf79 | |||
| fb0bb0102c | |||
| 80786b9f1f | |||
| d90b00d7ac | |||
| 8983fe5d8f | |||
| a26082d099 | |||
| f8b8f4feef | |||
| 5464e1a671 | |||
| c39b26050b | |||
| 9cc1e49d86 | |||
| 2c4e9985b1 | |||
| 821f1d20b3 | |||
| 555ced4fdc | |||
| 6b6a50b9ec | |||
| 4797f07af9 | |||
| 96331c9fa7 | |||
| a4e0b664e1 | |||
| f4bbddd0dd | |||
| 7c021c4eb5 | |||
| 5a97290356 | |||
| 32495b150b | |||
| a5404dc824 | |||
| b39280ee2a | |||
| 752d7ed3d0 | |||
| 618da593a6 | |||
| 1f317a0616 | |||
| 912239a97b |
@@ -16,20 +16,17 @@ permissions:
|
|||||||
security-events: write
|
security-events: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: git.farh.net
|
||||||
IMAGE_NAME: cartsnitch/cartsnitch
|
IMAGE_NAME: cartsnitch/cartsnitch
|
||||||
RECEIPTWITNESS_IMAGE_NAME: cartsnitch/receiptwitness
|
RECEIPTWITNESS_IMAGE_NAME: cartsnitch/receiptwitness
|
||||||
API_IMAGE_NAME: cartsnitch/api
|
API_IMAGE_NAME: cartsnitch/api
|
||||||
|
AUTH_IMAGE_NAME: cartsnitch/auth
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: ESLint
|
- name: ESLint
|
||||||
run: npx eslint .
|
run: npx eslint .
|
||||||
@@ -37,10 +34,10 @@ jobs:
|
|||||||
run: npx tsc --noEmit
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "20"
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -49,10 +46,10 @@ jobs:
|
|||||||
run: npx vitest run
|
run: npx vitest run
|
||||||
|
|
||||||
audit:
|
audit:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "20"
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -61,10 +58,10 @@ jobs:
|
|||||||
run: npm audit --audit-level=high
|
run: npm audit --audit-level=high
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "20"
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -73,11 +70,11 @@ jobs:
|
|||||||
- run: npx playwright test
|
- run: npx playwright test
|
||||||
|
|
||||||
lighthouse:
|
lighthouse:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: ubuntu-latest
|
||||||
needs: [test]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "20"
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -98,14 +95,14 @@ jobs:
|
|||||||
CHROME_PATH="$CHROME_PATH" lhci autorun --chrome-flags="--headless=new --no-sandbox --disable-gpu --disable-dev-shm-usage"
|
CHROME_PATH="$CHROME_PATH" lhci autorun --chrome-flags="--headless=new --no-sandbox --disable-gpu --disable-dev-shm-usage"
|
||||||
|
|
||||||
build-and-push:
|
build-and-push:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
needs: [lint, test, e2e]
|
needs: [lint, test, e2e]
|
||||||
outputs:
|
outputs:
|
||||||
calver_tag: ${{ steps.calver.outputs.version }}
|
calver_tag: ${{ steps.calver.outputs.version }}
|
||||||
sha_tag: sha-${{ github.sha }}
|
sha_tag: sha-${{ github.sha }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -133,13 +130,13 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to Gitea registry
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
@@ -159,8 +156,8 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
target: prod
|
target: prod
|
||||||
cache-from: type=gha
|
cache-from: type=inline
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=inline,mode=max
|
||||||
|
|
||||||
- name: Scan frontend image for vulnerabilities
|
- name: Scan frontend image for vulnerabilities
|
||||||
uses: anchore/scan-action@v5
|
uses: anchore/scan-action@v5
|
||||||
@@ -174,11 +171,7 @@ jobs:
|
|||||||
only-fixed: "true"
|
only-fixed: "true"
|
||||||
output-format: sarif
|
output-format: sarif
|
||||||
|
|
||||||
- name: Upload frontend scan results to GitHub Security
|
|
||||||
uses: github/codeql-action/upload-sarif@v3
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
sarif_file: ${{ steps.scan.outputs.sarif }}
|
|
||||||
|
|
||||||
- name: Push Docker image
|
- name: Push Docker image
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
@@ -189,7 +182,7 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
target: prod
|
target: prod
|
||||||
cache-from: type=gha
|
cache-from: type=inline
|
||||||
|
|
||||||
- name: Create git tag
|
- name: Create git tag
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
@@ -198,14 +191,14 @@ jobs:
|
|||||||
git push origin "v${{ steps.calver.outputs.version }}"
|
git push origin "v${{ steps.calver.outputs.version }}"
|
||||||
|
|
||||||
build-and-push-receiptwitness:
|
build-and-push-receiptwitness:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
needs: [lint, test]
|
needs: [lint, test]
|
||||||
outputs:
|
outputs:
|
||||||
calver_tag: ${{ steps.calver.outputs.version }}
|
calver_tag: ${{ steps.calver.outputs.version }}
|
||||||
sha_tag: sha-${{ github.sha }}
|
sha_tag: sha-${{ github.sha }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -227,13 +220,13 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to Gitea registry
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
@@ -255,8 +248,8 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
build-args: |
|
build-args: |
|
||||||
APT_CACHE_BUST=${{ github.run_id }}
|
APT_CACHE_BUST=${{ github.run_id }}
|
||||||
cache-from: type=gha
|
cache-from: type=inline
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=inline,mode=max
|
||||||
|
|
||||||
- name: Scan receiptwitness image for vulnerabilities
|
- name: Scan receiptwitness image for vulnerabilities
|
||||||
uses: anchore/scan-action@v5
|
uses: anchore/scan-action@v5
|
||||||
@@ -270,11 +263,7 @@ jobs:
|
|||||||
only-fixed: "true"
|
only-fixed: "true"
|
||||||
output-format: sarif
|
output-format: sarif
|
||||||
|
|
||||||
- name: Upload receiptwitness scan results to GitHub Security
|
|
||||||
uses: github/codeql-action/upload-sarif@v3
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
sarif_file: ${{ steps.scan.outputs.sarif }}
|
|
||||||
|
|
||||||
- name: Push Docker image
|
- name: Push Docker image
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
@@ -287,17 +276,17 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
build-args: |
|
build-args: |
|
||||||
APT_CACHE_BUST=${{ github.run_id }}
|
APT_CACHE_BUST=${{ github.run_id }}
|
||||||
cache-from: type=gha
|
cache-from: type=inline
|
||||||
|
|
||||||
build-and-push-api:
|
build-and-push-api:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
needs: [lint, test]
|
needs: [lint, test]
|
||||||
outputs:
|
outputs:
|
||||||
calver_tag: ${{ steps.calver.outputs.version }}
|
calver_tag: ${{ steps.calver.outputs.version }}
|
||||||
sha_tag: sha-${{ github.sha }}
|
sha_tag: sha-${{ github.sha }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -319,13 +308,13 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to Gitea registry
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata (API)
|
- name: Extract metadata (API)
|
||||||
id: meta
|
id: meta
|
||||||
@@ -347,8 +336,8 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
build-args: |
|
build-args: |
|
||||||
APT_CACHE_BUST=${{ github.run_id }}
|
APT_CACHE_BUST=${{ github.run_id }}
|
||||||
cache-from: type=gha
|
cache-from: type=inline
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=inline,mode=max
|
||||||
|
|
||||||
- name: Scan api image for vulnerabilities
|
- name: Scan api image for vulnerabilities
|
||||||
uses: anchore/scan-action@v5
|
uses: anchore/scan-action@v5
|
||||||
@@ -362,11 +351,7 @@ jobs:
|
|||||||
only-fixed: "true"
|
only-fixed: "true"
|
||||||
output-format: sarif
|
output-format: sarif
|
||||||
|
|
||||||
- name: Upload api scan results to GitHub Security
|
|
||||||
uses: github/codeql-action/upload-sarif@v3
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
sarif_file: ${{ steps.scan.outputs.sarif }}
|
|
||||||
|
|
||||||
- name: Push Docker image
|
- name: Push Docker image
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
@@ -379,27 +364,106 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
build-args: |
|
build-args: |
|
||||||
APT_CACHE_BUST=${{ github.run_id }}
|
APT_CACHE_BUST=${{ github.run_id }}
|
||||||
cache-from: type=gha
|
cache-from: type=inline
|
||||||
|
|
||||||
|
build-and-push-auth:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
needs: [lint, test]
|
||||||
|
outputs:
|
||||||
|
calver_tag: ${{ steps.calver.outputs.version }}
|
||||||
|
sha_tag: sha-${{ github.sha }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Generate CalVer tag
|
||||||
|
id: calver
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
run: |
|
||||||
|
DATE_TAG=$(date -u +%Y.%m.%d)
|
||||||
|
EXISTING=$(git tag -l "v${DATE_TAG}*" | sort -V | tail -1)
|
||||||
|
if [ -z "$EXISTING" ]; then VERSION="$DATE_TAG"
|
||||||
|
elif [ "$EXISTING" = "v${DATE_TAG}" ]; then VERSION="${DATE_TAG}.2"
|
||||||
|
else BUILD_NUM=$(echo "$EXISTING" | sed "s/v${DATE_TAG}\.//"); VERSION="${DATE_TAG}.$((BUILD_NUM + 1))"; fi
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to Gitea registry
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (auth)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.AUTH_IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=sha,prefix=sha-,format=long
|
||||||
|
type=raw,value=${{ steps.calver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./auth
|
||||||
|
file: ./auth/Dockerfile
|
||||||
|
load: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
APT_CACHE_BUST=${{ github.run_id }}
|
||||||
|
cache-from: type=inline
|
||||||
|
cache-to: type=inline,mode=max
|
||||||
|
|
||||||
|
- name: Scan auth image for vulnerabilities
|
||||||
|
uses: anchore/scan-action@v5
|
||||||
|
id: scan
|
||||||
|
env:
|
||||||
|
GRYPE_CONFIG: .grype.yaml
|
||||||
|
with:
|
||||||
|
image: "${{ env.REGISTRY }}/${{ env.AUTH_IMAGE_NAME }}:sha-${{ github.sha }}"
|
||||||
|
fail-build: true
|
||||||
|
severity-cutoff: high
|
||||||
|
only-fixed: "true"
|
||||||
|
output-format: sarif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: Push Docker image
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./auth
|
||||||
|
file: ./auth/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
APT_CACHE_BUST=${{ github.run_id }}
|
||||||
|
cache-from: type=inline
|
||||||
|
|
||||||
deploy-dev:
|
deploy-dev:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: ubuntu-latest
|
||||||
needs: [build-and-push, build-and-push-receiptwitness, build-and-push-api]
|
needs: [build-and-push, build-and-push-receiptwitness, build-and-push-api, build-and-push-auth]
|
||||||
if: always() && !cancelled() && github.event_name == 'push' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main')
|
if: always() && !cancelled() && github.event_name == 'push' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main')
|
||||||
steps:
|
steps:
|
||||||
- name: Generate GitHub App token
|
|
||||||
id: app-token
|
|
||||||
uses: actions/create-github-app-token@v1
|
|
||||||
with:
|
|
||||||
app-id: ${{ secrets.CARTSNITCH_APP_ID }}
|
|
||||||
private-key: ${{ secrets.CARTSNITCH_APP_PRIVATE_KEY }}
|
|
||||||
owner: ${{ github.repository_owner }}
|
|
||||||
repositories: infra
|
|
||||||
|
|
||||||
- name: Checkout infra repo
|
- name: Checkout infra repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
with:
|
||||||
repository: cartsnitch/infra
|
repository: cartsnitch/infra
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
token: ${{ secrets.CI_GITEA_TOKEN }}
|
||||||
ref: main
|
ref: main
|
||||||
path: infra
|
path: infra
|
||||||
|
|
||||||
@@ -407,7 +471,16 @@ jobs:
|
|||||||
uses: azure/setup-kubectl@v4
|
uses: azure/setup-kubectl@v4
|
||||||
|
|
||||||
- name: Install kustomize
|
- name: Install kustomize
|
||||||
uses: imranismail/setup-kustomize@v2
|
# imranismail/setup-kustomize@v2 calls the Gitea API to record
|
||||||
|
# telemetry under the "kubernetes-sigs" user, which doesn't exist
|
||||||
|
# on this Gitea instance. Install the binary directly instead.
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
version="5.4.3"
|
||||||
|
url="https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${version}/kustomize_v${version}_linux_amd64.tar.gz"
|
||||||
|
curl -fsSL --retry 3 "$url" | tar -xz -C /tmp kustomize
|
||||||
|
sudo install -m 0755 /tmp/kustomize /usr/local/bin/kustomize
|
||||||
|
kustomize version
|
||||||
|
|
||||||
- name: Determine image tag for frontend
|
- name: Determine image tag for frontend
|
||||||
id: frontend_tag
|
id: frontend_tag
|
||||||
@@ -422,7 +495,7 @@ jobs:
|
|||||||
if: needs.build-and-push.result == 'success'
|
if: needs.build-and-push.result == 'success'
|
||||||
run: |
|
run: |
|
||||||
cd infra/apps/overlays/dev
|
cd infra/apps/overlays/dev
|
||||||
kustomize edit set image ghcr.io/cartsnitch/cartsnitch:${{ steps.frontend_tag.outputs.tag }}
|
kustomize edit set image ghcr.io/cartsnitch/cartsnitch=git.farh.net/cartsnitch/cartsnitch:${{ steps.frontend_tag.outputs.tag }}
|
||||||
|
|
||||||
- name: Determine image tag for receiptwitness
|
- name: Determine image tag for receiptwitness
|
||||||
id: receiptwitness_tag
|
id: receiptwitness_tag
|
||||||
@@ -437,7 +510,7 @@ jobs:
|
|||||||
if: needs.build-and-push-receiptwitness.result == 'success'
|
if: needs.build-and-push-receiptwitness.result == 'success'
|
||||||
run: |
|
run: |
|
||||||
cd infra/apps/overlays/dev
|
cd infra/apps/overlays/dev
|
||||||
kustomize edit set image ghcr.io/cartsnitch/receiptwitness:${{ steps.receiptwitness_tag.outputs.tag }}
|
kustomize edit set image ghcr.io/cartsnitch/receiptwitness=git.farh.net/cartsnitch/receiptwitness:${{ steps.receiptwitness_tag.outputs.tag }}
|
||||||
|
|
||||||
- name: Determine image tag for api
|
- name: Determine image tag for api
|
||||||
id: api_tag
|
id: api_tag
|
||||||
@@ -452,38 +525,89 @@ jobs:
|
|||||||
if: needs.build-and-push-api.result == 'success'
|
if: needs.build-and-push-api.result == 'success'
|
||||||
run: |
|
run: |
|
||||||
cd infra/apps/overlays/dev
|
cd infra/apps/overlays/dev
|
||||||
kustomize edit set image ghcr.io/cartsnitch/api:${{ steps.api_tag.outputs.tag }}
|
kustomize edit set image ghcr.io/cartsnitch/api=git.farh.net/cartsnitch/api:${{ steps.api_tag.outputs.tag }}
|
||||||
|
|
||||||
- name: Commit and push to infra
|
- name: Determine image tag for auth
|
||||||
|
id: auth_tag
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
|
||||||
|
echo "tag=${{ needs.build-and-push-auth.outputs.calver_tag }}" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "tag=${{ needs.build-and-push-auth.outputs.sha_tag }}" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update auth image tag
|
||||||
|
if: needs.build-and-push-auth.result == 'success'
|
||||||
|
run: |
|
||||||
|
cd infra/apps/overlays/dev
|
||||||
|
kustomize edit set image ghcr.io/cartsnitch/auth=git.farh.net/cartsnitch/auth:${{ steps.auth_tag.outputs.tag }}
|
||||||
|
|
||||||
|
- name: Commit and push to infra (via PR)
|
||||||
|
env:
|
||||||
|
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
cd infra
|
cd infra
|
||||||
git config user.name "cartsnitch-ci[bot]"
|
git config user.name "cartsnitch-ci[bot]"
|
||||||
git config user.email "cartsnitch-ci[bot]@users.noreply.github.com"
|
git config user.email "cartsnitch-ci[bot]@users.noreply.git.farh.net"
|
||||||
git add apps/overlays/dev/kustomization.yaml
|
git add apps/overlays/dev/kustomization.yaml
|
||||||
git diff --cached --quiet && echo "No image changes to deploy" && exit 0
|
git diff --cached --quiet && echo "No image changes to deploy" && exit 0
|
||||||
git commit -m "ci(dev): update cartsnitch, receiptwitness, and api images"
|
BRANCH="ci/deploy-dev-${GITHUB_SHA}"
|
||||||
git pull --rebase origin main
|
git checkout -b "$BRANCH"
|
||||||
git push origin main
|
git commit -m "ci(dev): update cartsnitch, receiptwitness, api, and auth images"
|
||||||
|
git push origin "$BRANCH"
|
||||||
|
PR_BODY=$(printf 'Auto-opened by deploy-dev (CAR-1195).\n\nBuild SHA: %s' "${GITHUB_SHA}")
|
||||||
|
PR_JSON=$(curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${CI_GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$(jq -n --arg head "cartsnitch:${BRANCH}" --arg base main --arg title "ci(dev): update overlay image tags (${GITHUB_SHA::12})" --arg body "$PR_BODY" '{head:$head,base:$base,title:$title,body:$body}')" \
|
||||||
|
"https://git.farh.net/api/v1/repos/cartsnitch/infra/pulls")
|
||||||
|
PR_NUM=$(echo "$PR_JSON" | jq -r '.number // empty')
|
||||||
|
if [ -z "$PR_NUM" ]; then
|
||||||
|
echo "::error::Failed to open PR against cartsnitch/infra: $PR_JSON"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Opened cartsnitch/infra PR #${PR_NUM} (head=${BRANCH})"
|
||||||
|
# Request CTO (cs_savannah) review as the GitOps hand-off. Best-effort:
|
||||||
|
# log on non-2xx but never fail the job for this.
|
||||||
|
REVIEW_HTTP=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${CI_GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"reviewers":["cs_savannah"]}' \
|
||||||
|
"https://git.farh.net/api/v1/repos/cartsnitch/infra/pulls/${PR_NUM}/requested_reviewers")
|
||||||
|
if [ "${REVIEW_HTTP}" -lt 200 ] || [ "${REVIEW_HTTP}" -ge 300 ]; then
|
||||||
|
echo "::notice::Failed to request reviewers for cartsnitch/infra PR #${PR_NUM} (HTTP ${REVIEW_HTTP}); continuing"
|
||||||
|
fi
|
||||||
|
MERGE_RESP=$(curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${CI_GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"Do":"merge","delete_branch_after_merge":true}' \
|
||||||
|
"https://git.farh.net/api/v1/repos/cartsnitch/infra/pulls/${PR_NUM}/merge")
|
||||||
|
MERGED=$(echo "$MERGE_RESP" | jq -r '.merged // false')
|
||||||
|
if [ "$MERGED" = "true" ]; then
|
||||||
|
echo "PR #${PR_NUM} merged into cartsnitch/infra main"
|
||||||
|
elif echo "$MERGE_RESP" | grep -qi 'does not have enough approvals'; then
|
||||||
|
# GitOps approval gate: the PR is correctly opened and surfaces in
|
||||||
|
# the CTO queue via the reviewers request above. Treat as success
|
||||||
|
# (exit 0) so the deploy job does not hard-fail on the approvals
|
||||||
|
# requirement that only a human maintainer can satisfy.
|
||||||
|
echo "::notice::infra PR #${PR_NUM} opened and awaiting CTO (cs_savannah) approve+merge — GitOps approval gate, not a failure"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "::error::Auto-merge of cartsnitch/infra PR #${PR_NUM} failed: $MERGE_RESP"
|
||||||
|
echo "::error::Reassign to cs_savannah (authorized merger for cartsnitch/infra main) for backstop merge."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
deploy-uat:
|
deploy-uat:
|
||||||
runs-on: runners-cartsnitch
|
runs-on: ubuntu-latest
|
||||||
needs: [build-and-push, build-and-push-receiptwitness, build-and-push-api]
|
needs: [build-and-push, build-and-push-receiptwitness, build-and-push-api, build-and-push-auth]
|
||||||
if: always() && !cancelled() && github.event_name == 'push' && (github.ref == 'refs/heads/uat' || github.ref == 'refs/heads/main')
|
if: always() && !cancelled() && github.event_name == 'push' && (github.ref == 'refs/heads/uat' || github.ref == 'refs/heads/main')
|
||||||
steps:
|
steps:
|
||||||
- name: Generate GitHub App token
|
|
||||||
id: app-token
|
|
||||||
uses: actions/create-github-app-token@v1
|
|
||||||
with:
|
|
||||||
app-id: ${{ secrets.CARTSNITCH_APP_ID }}
|
|
||||||
private-key: ${{ secrets.CARTSNITCH_APP_PRIVATE_KEY }}
|
|
||||||
owner: ${{ github.repository_owner }}
|
|
||||||
repositories: infra
|
|
||||||
|
|
||||||
- name: Checkout infra repo
|
- name: Checkout infra repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
with:
|
||||||
repository: cartsnitch/infra
|
repository: cartsnitch/infra
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
token: ${{ secrets.CI_GITEA_TOKEN }}
|
||||||
ref: main
|
ref: main
|
||||||
path: infra
|
path: infra
|
||||||
|
|
||||||
@@ -491,7 +615,16 @@ jobs:
|
|||||||
uses: azure/setup-kubectl@v4
|
uses: azure/setup-kubectl@v4
|
||||||
|
|
||||||
- name: Install kustomize
|
- name: Install kustomize
|
||||||
uses: imranismail/setup-kustomize@v2
|
# imranismail/setup-kustomize@v2 calls the Gitea API to record
|
||||||
|
# telemetry under the "kubernetes-sigs" user, which doesn't exist
|
||||||
|
# on this Gitea instance. Install the binary directly instead.
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
version="5.4.3"
|
||||||
|
url="https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${version}/kustomize_v${version}_linux_amd64.tar.gz"
|
||||||
|
curl -fsSL --retry 3 "$url" | tar -xz -C /tmp kustomize
|
||||||
|
sudo install -m 0755 /tmp/kustomize /usr/local/bin/kustomize
|
||||||
|
kustomize version
|
||||||
|
|
||||||
- name: Determine image tag for frontend
|
- name: Determine image tag for frontend
|
||||||
id: frontend_tag
|
id: frontend_tag
|
||||||
@@ -506,7 +639,7 @@ jobs:
|
|||||||
if: needs.build-and-push.result == 'success'
|
if: needs.build-and-push.result == 'success'
|
||||||
run: |
|
run: |
|
||||||
cd infra/apps/overlays/uat
|
cd infra/apps/overlays/uat
|
||||||
kustomize edit set image ghcr.io/cartsnitch/cartsnitch:${{ steps.frontend_tag.outputs.tag }}
|
kustomize edit set image ghcr.io/cartsnitch/cartsnitch=git.farh.net/cartsnitch/cartsnitch:${{ steps.frontend_tag.outputs.tag }}
|
||||||
|
|
||||||
- name: Determine image tag for receiptwitness
|
- name: Determine image tag for receiptwitness
|
||||||
id: receiptwitness_tag
|
id: receiptwitness_tag
|
||||||
@@ -521,7 +654,7 @@ jobs:
|
|||||||
if: needs.build-and-push-receiptwitness.result == 'success'
|
if: needs.build-and-push-receiptwitness.result == 'success'
|
||||||
run: |
|
run: |
|
||||||
cd infra/apps/overlays/uat
|
cd infra/apps/overlays/uat
|
||||||
kustomize edit set image ghcr.io/cartsnitch/receiptwitness:${{ steps.receiptwitness_tag.outputs.tag }}
|
kustomize edit set image ghcr.io/cartsnitch/receiptwitness=git.farh.net/cartsnitch/receiptwitness:${{ steps.receiptwitness_tag.outputs.tag }}
|
||||||
|
|
||||||
- name: Determine image tag for api
|
- name: Determine image tag for api
|
||||||
id: api_tag
|
id: api_tag
|
||||||
@@ -536,15 +669,75 @@ jobs:
|
|||||||
if: needs.build-and-push-api.result == 'success'
|
if: needs.build-and-push-api.result == 'success'
|
||||||
run: |
|
run: |
|
||||||
cd infra/apps/overlays/uat
|
cd infra/apps/overlays/uat
|
||||||
kustomize edit set image ghcr.io/cartsnitch/api:${{ steps.api_tag.outputs.tag }}
|
kustomize edit set image ghcr.io/cartsnitch/api=git.farh.net/cartsnitch/api:${{ steps.api_tag.outputs.tag }}
|
||||||
|
|
||||||
- name: Commit and push to infra
|
- name: Determine image tag for auth
|
||||||
|
id: auth_tag
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
|
||||||
|
echo "tag=${{ needs.build-and-push-auth.outputs.calver_tag }}" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "tag=${{ needs.build-and-push-auth.outputs.sha_tag }}" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update auth image tag
|
||||||
|
if: needs.build-and-push-auth.result == 'success'
|
||||||
|
run: |
|
||||||
|
cd infra/apps/overlays/uat
|
||||||
|
kustomize edit set image ghcr.io/cartsnitch/auth=git.farh.net/cartsnitch/auth:${{ steps.auth_tag.outputs.tag }}
|
||||||
|
|
||||||
|
- name: Commit and push to infra (via PR)
|
||||||
|
env:
|
||||||
|
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
cd infra
|
cd infra
|
||||||
git config user.name "cartsnitch-ci[bot]"
|
git config user.name "cartsnitch-ci[bot]"
|
||||||
git config user.email "cartsnitch-ci[bot]@users.noreply.github.com"
|
git config user.email "cartsnitch-ci[bot]@users.noreply.git.farh.net"
|
||||||
git add apps/overlays/uat/kustomization.yaml
|
git add apps/overlays/uat/kustomization.yaml
|
||||||
git diff --cached --quiet && echo "No image changes to deploy" && exit 0
|
git diff --cached --quiet && echo "No image changes to deploy" && exit 0
|
||||||
git commit -m "ci(uat): update cartsnitch, receiptwitness, and api images"
|
BRANCH="ci/deploy-uat-${GITHUB_SHA}"
|
||||||
git pull --rebase origin main
|
git checkout -b "$BRANCH"
|
||||||
git push origin main
|
git commit -m "ci(uat): update cartsnitch, receiptwitness, api, and auth images"
|
||||||
|
git push origin "$BRANCH"
|
||||||
|
PR_BODY=$(printf 'Auto-opened by deploy-uat (CAR-1195).\n\nBuild SHA: %s' "${GITHUB_SHA}")
|
||||||
|
PR_JSON=$(curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${CI_GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$(jq -n --arg head "cartsnitch:${BRANCH}" --arg base main --arg title "ci(uat): update overlay image tags (${GITHUB_SHA::12})" --arg body "$PR_BODY" '{head:$head,base:$base,title:$title,body:$body}')" \
|
||||||
|
"https://git.farh.net/api/v1/repos/cartsnitch/infra/pulls")
|
||||||
|
PR_NUM=$(echo "$PR_JSON" | jq -r '.number // empty')
|
||||||
|
if [ -z "$PR_NUM" ]; then
|
||||||
|
echo "::error::Failed to open PR against cartsnitch/infra: $PR_JSON"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Opened cartsnitch/infra PR #${PR_NUM} (head=${BRANCH})"
|
||||||
|
# Request CTO (cs_savannah) review as the GitOps hand-off. Best-effort:
|
||||||
|
# log on non-2xx but never fail the job for this.
|
||||||
|
REVIEW_HTTP=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${CI_GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"reviewers":["cs_savannah"]}' \
|
||||||
|
"https://git.farh.net/api/v1/repos/cartsnitch/infra/pulls/${PR_NUM}/requested_reviewers")
|
||||||
|
if [ "${REVIEW_HTTP}" -lt 200 ] || [ "${REVIEW_HTTP}" -ge 300 ]; then
|
||||||
|
echo "::notice::Failed to request reviewers for cartsnitch/infra PR #${PR_NUM} (HTTP ${REVIEW_HTTP}); continuing"
|
||||||
|
fi
|
||||||
|
MERGE_RESP=$(curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${CI_GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"Do":"merge","delete_branch_after_merge":true}' \
|
||||||
|
"https://git.farh.net/api/v1/repos/cartsnitch/infra/pulls/${PR_NUM}/merge")
|
||||||
|
MERGED=$(echo "$MERGE_RESP" | jq -r '.merged // false')
|
||||||
|
if [ "$MERGED" = "true" ]; then
|
||||||
|
echo "PR #${PR_NUM} merged into cartsnitch/infra main"
|
||||||
|
elif echo "$MERGE_RESP" | grep -qi 'does not have enough approvals'; then
|
||||||
|
# GitOps approval gate: the PR is correctly opened and surfaces in
|
||||||
|
# the CTO queue via the reviewers request above. Treat as success
|
||||||
|
# (exit 0) so the deploy job does not hard-fail on the approvals
|
||||||
|
# requirement that only a human maintainer can satisfy.
|
||||||
|
echo "::notice::infra PR #${PR_NUM} opened and awaiting CTO (cs_savannah) approve+merge — GitOps approval gate, not a failure"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "::error::Auto-merge of cartsnitch/infra PR #${PR_NUM} failed: $MERGE_RESP"
|
||||||
|
echo "::error::Reassign to cs_savannah (authorized merger for cartsnitch/infra main) for backstop merge."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -15,7 +15,7 @@ permissions:
|
|||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: git.farh.net
|
||||||
IMAGE_NAME: cartsnitch/api
|
IMAGE_NAME: cartsnitch/api
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -130,13 +130,13 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to Gitea registry
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
@@ -6,7 +6,6 @@ from fastapi import APIRouter, FastAPI
|
|||||||
|
|
||||||
from cartsnitch_api.auth.routes import router as auth_router
|
from cartsnitch_api.auth.routes import router as auth_router
|
||||||
from cartsnitch_api.cache import cache_client
|
from cartsnitch_api.cache import cache_client
|
||||||
from cartsnitch_api.database import dispose_engine
|
|
||||||
from cartsnitch_api.middleware.cors import add_cors_middleware
|
from cartsnitch_api.middleware.cors import add_cors_middleware
|
||||||
from cartsnitch_api.middleware.error_handler import add_error_handlers, add_error_monitor_middleware
|
from cartsnitch_api.middleware.error_handler import add_error_handlers, add_error_monitor_middleware
|
||||||
from cartsnitch_api.middleware.rate_limit import add_rate_limit_middleware
|
from cartsnitch_api.middleware.rate_limit import add_rate_limit_middleware
|
||||||
|
|||||||
@@ -19,9 +19,18 @@ describe('Auth health endpoint', () => {
|
|||||||
}
|
}
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ status: 'ok', db: 'reachable' }));
|
res.end(JSON.stringify({ status: 'ok', db: 'reachable' }));
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
// Mirror src/index.ts: log the error and include the message in the
|
||||||
|
// response body so /health 503s are diagnosable from pod logs.
|
||||||
|
console.error(
|
||||||
|
'[auth /health] DB probe failed:',
|
||||||
|
err instanceof Error ? `${err.name}: ${err.message}` : err,
|
||||||
|
);
|
||||||
|
const detail = err instanceof Error ? err.message : 'unknown error';
|
||||||
res.writeHead(503, { 'Content-Type': 'application/json' });
|
res.writeHead(503, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ status: 'error', db: 'unreachable' }));
|
res.end(
|
||||||
|
JSON.stringify({ status: 'error', db: 'unreachable', error: detail }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -76,7 +85,10 @@ describe('Auth health endpoint', () => {
|
|||||||
close();
|
close();
|
||||||
|
|
||||||
equal(status, 503);
|
equal(status, 503);
|
||||||
equal(body, '{"status":"error","db":"unreachable"}');
|
const parsed = JSON.parse(body);
|
||||||
|
equal(parsed.status, 'error');
|
||||||
|
equal(parsed.db, 'unreachable');
|
||||||
|
equal(parsed.error, 'connection refused');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 503 with db=unreachable when query times out', async () => {
|
it('returns 503 with db=unreachable when query times out', async () => {
|
||||||
@@ -95,7 +107,14 @@ describe('Auth health endpoint', () => {
|
|||||||
close();
|
close();
|
||||||
|
|
||||||
equal(status, 503);
|
equal(status, 503);
|
||||||
equal(body, '{"status":"error","db":"unreachable"}');
|
const parsed = JSON.parse(body);
|
||||||
|
equal(parsed.status, 'error');
|
||||||
|
equal(parsed.db, 'unreachable');
|
||||||
|
// The query promise rejects with a synthetic 'timeout' error; the
|
||||||
|
// Promise.race wrapper also rejects with 'DB timeout'. The body should
|
||||||
|
// surface whichever error was thrown — accept either to stay robust.
|
||||||
|
equal(typeof parsed.error, 'string');
|
||||||
|
equal(parsed.error.length > 0, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a terminal response for unknown paths (no hang)', async () => {
|
it('returns a terminal response for unknown paths (no hang)', async () => {
|
||||||
|
|||||||
+12
-2
@@ -21,9 +21,19 @@ const server = createServer(async (req, res) => {
|
|||||||
}
|
}
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
res.end(JSON.stringify({ status: "ok", db: "reachable" }));
|
res.end(JSON.stringify({ status: "ok", db: "reachable" }));
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
// Log the actual error so /health 503s are diagnosable from pod logs
|
||||||
|
// (CAR-1276: UAT auth was crashlooping with no log output beyond the
|
||||||
|
// initial "listening on port 3001" line because this catch was empty).
|
||||||
|
console.error(
|
||||||
|
"[auth /health] DB probe failed:",
|
||||||
|
err instanceof Error ? `${err.name}: ${err.message}` : err,
|
||||||
|
);
|
||||||
|
const detail = err instanceof Error ? err.message : "unknown error";
|
||||||
res.writeHead(503, { "Content-Type": "application/json" });
|
res.writeHead(503, { "Content-Type": "application/json" });
|
||||||
res.end(JSON.stringify({ status: "error", db: "unreachable" }));
|
res.end(
|
||||||
|
JSON.stringify({ status: "error", db: "unreachable", error: detail }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -12,5 +12,5 @@
|
|||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist", "src/__tests__"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,36 @@
|
|||||||
"""Database engine and session factories for sync and async usage."""
|
"""Database engine and session factories for sync and async usage."""
|
||||||
|
|
||||||
from collections.abc import AsyncGenerator, Generator
|
from collections.abc import AsyncGenerator, Generator
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.orm import Session, sessionmaker
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
|
|
||||||
from cartsnitch_common.config import settings
|
from cartsnitch_common.config import settings
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
|
||||||
def get_async_engine(url: str | None = None):
|
# Module-level async engine cache — one engine per unique URL, shared across all callers.
|
||||||
"""Create an async SQLAlchemy engine."""
|
# This prevents pool exhaustion in high-throughput workers (e.g. email-worker hitting
|
||||||
return create_async_engine(url or settings.database_url, echo=settings.debug)
|
# DragonflyDB/Postgres repeatedly per message). pool_size=10, max_overflow=20 gives
|
||||||
|
# headroom for bursts while capping max connections at 30 per URL.
|
||||||
|
_async_engine_cache: dict[str, "AsyncEngine"] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_async_engine(url: str | None = None) -> "AsyncEngine":
|
||||||
|
"""Get or create a cached async engine for the given URL."""
|
||||||
|
target = url or settings.database_url
|
||||||
|
if target not in _async_engine_cache:
|
||||||
|
_async_engine_cache[target] = create_async_engine(
|
||||||
|
target,
|
||||||
|
echo=settings.debug,
|
||||||
|
pool_size=10,
|
||||||
|
max_overflow=20,
|
||||||
|
pool_pre_ping=True,
|
||||||
|
)
|
||||||
|
return _async_engine_cache[target]
|
||||||
|
|
||||||
|
|
||||||
def get_sync_engine(url: str | None = None):
|
def get_sync_engine(url: str | None = None):
|
||||||
|
|||||||
Generated
+297
-391
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -45,14 +45,16 @@
|
|||||||
"typescript-eslint": "^8.56.1",
|
"typescript-eslint": "^8.56.1",
|
||||||
"vite": "^6.4.2",
|
"vite": "^6.4.2",
|
||||||
"vite-plugin-pwa": "^0.21.2",
|
"vite-plugin-pwa": "^0.21.2",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^4.1.8"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@rollup/pluginutils": "5.3.0",
|
"@rollup/pluginutils": "5.3.0",
|
||||||
"flatted": "^3.4.2",
|
"flatted": "^3.4.2",
|
||||||
"serialize-javascript": "7.0.5",
|
"serialize-javascript": "7.0.5",
|
||||||
"brace-expansion": ">=1.1.13",
|
"brace-expansion": ">=5.0.6",
|
||||||
"lodash": ">=4.17.24",
|
"lodash": ">=4.17.24",
|
||||||
"minimatch": "^10.2.4"
|
"minimatch": "^10.2.4",
|
||||||
|
"@babel/plugin-transform-modules-systemjs": "^7.29.4",
|
||||||
|
"fast-uri": "^3.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,29 @@ logger = logging.getLogger(__name__)
|
|||||||
STREAM_KEY = "email:receipts"
|
STREAM_KEY = "email:receipts"
|
||||||
CONSUMER_GROUP = "email-workers"
|
CONSUMER_GROUP = "email-workers"
|
||||||
|
|
||||||
|
# Module-level Redis/DragonflyDB connection pool — shared across all worker calls.
|
||||||
|
# Without pooling, each call to get_redis() opens a new TCP connection. In a tight
|
||||||
|
# consumer loop this causes ConnectionResetError when DragonflyDB's connection limit
|
||||||
|
# is hit under load. max_connections=30 (10 base + 20 overflow) mirrors the engine pool.
|
||||||
|
_redis_pool: aioredis.ConnectionPool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_redis_pool() -> aioredis.ConnectionPool:
|
||||||
|
"""Get or create the shared DragonflyDB connection pool."""
|
||||||
|
global _redis_pool
|
||||||
|
if _redis_pool is None:
|
||||||
|
_redis_pool = aioredis.ConnectionPool.from_url(
|
||||||
|
settings.redis_url,
|
||||||
|
decode_responses=True,
|
||||||
|
max_connections=30,
|
||||||
|
)
|
||||||
|
return _redis_pool
|
||||||
|
|
||||||
|
|
||||||
|
async def get_redis() -> aioredis.Redis:
|
||||||
|
"""Get async Redis/DragonflyDB client backed by a shared connection pool."""
|
||||||
|
return aioredis.Redis(connection_pool=_get_redis_pool())
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EmailJob:
|
class EmailJob:
|
||||||
@@ -31,11 +54,6 @@ class EmailJob:
|
|||||||
message_id: str # from email provider, for dedup
|
message_id: str # from email provider, for dedup
|
||||||
|
|
||||||
|
|
||||||
async def get_redis() -> aioredis.Redis:
|
|
||||||
"""Get async Redis/DragonflyDB client."""
|
|
||||||
return cast(aioredis.Redis, aioredis.from_url(settings.redis_url, decode_responses=True))
|
|
||||||
|
|
||||||
|
|
||||||
async def ensure_consumer_group(client: aioredis.Redis) -> None:
|
async def ensure_consumer_group(client: aioredis.Redis) -> None:
|
||||||
"""Create consumer group if it does not exist."""
|
"""Create consumer group if it does not exist."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user