Skip to content

fix(db): populate book.Author in List and Get responses (closes #882)… #1819

fix(db): populate book.Author in List and Get responses (closes #882)…

fix(db): populate book.Author in List and Get responses (closes #882)… #1819

Workflow file for this run

name: CI
on:
push:
branches: [main, development]
tags: ['v*']
pull_request:
permissions:
contents: read
jobs:
# ── Parallel checks — all events ───────────────────────────────────────────
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.25.10"
cache: true
- uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: v2.11.4
args: --timeout=5m
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@d1f380186385b4f64e00313f31743df8e4b89a77
govulncheck ./...
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci --prefix web
- run: npm run lint --prefix web
- run: npm run typecheck --prefix web
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.25.10"
cache: true
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: npm
cache-dependency-path: web/package-lock.json
- run: make smoke
# PR-only: Go tests with coverage. Race-detector runs as a separate
# parallel job (validate-race-go) so neither stacks the other's overhead.
# Combined on one job they were tripping the 10-minute default timeout
# in CI when both -race and -covermode=atomic compounded.
validate-go:
name: validate (Go)
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.25.10"
cache: true
- run: go test -coverprofile=coverage.out -covermode=atomic ./cmd/... ./internal/...
- uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5
with:
files: coverage.out
flags: backend
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
# PR-only race detector. Runs in parallel with validate-go so the two
# checks don't serialise. Matches the post-merge race job at line 125
# (no coverage flag, just -race).
validate-race-go:
name: validate (Go race)
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.25.10"
cache: true
- run: go test -race -timeout=15m ./cmd/... ./internal/...
validate-frontend:
name: validate (frontend)
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci --prefix web
- run: npm run build --prefix web
# ── Gate: fast tests that must pass before image + release ─────────────────
test:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/development' || startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.25.10"
cache: true
- run: go test -coverprofile=coverage.out -covermode=atomic ./cmd/... ./internal/...
- uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5
with:
files: coverage.out
flags: backend
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci --prefix web
- run: npm run build --prefix web
# Race-detector run — parallel with image/goreleaser, not a deploy gate.
# A failure shows as a non-blocking check; fix before the next release.
race:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.25.10"
cache: true
- run: go test -race -timeout=15m ./cmd/... ./internal/...
# ── Docker image — runs after test passes ──────────────────────────────────
image:
needs: test
runs-on: ubuntu-latest
permissions:
packages: write
id-token: write
attestations: write
env:
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
if: env.DOCKERHUB_TOKEN != ''
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
id: meta
with:
images: |
ghcr.io/${{ github.repository }}
vavallee/bindery
tags: |
type=sha,format=short
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=development,enable=${{ github.ref == 'refs/heads/development' }}
type=semver,pattern={{version}}
type=semver,pattern=v{{version}}
- name: Derive version
id: ver
run: |
short="${GITHUB_SHA::7}"
if described=$(git describe --tags --match 'v*' --always 2>/dev/null) && [[ "$described" == v* ]]; then
echo "version=${described}" >> "$GITHUB_OUTPUT"
elif [[ "$GITHUB_REF" == refs/heads/development ]]; then
echo "version=dev-${short}" >> "$GITHUB_OUTPUT"
else
echo "version=sha-${short}" >> "$GITHUB_OUTPUT"
fi
- uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
id: push
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ steps.ver.outputs.version }}
COMMIT=${{ github.sha }}
BUILD_DATE=${{ github.event.head_commit.timestamp }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Attest build provenance
uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
# ── Deploy to bindery-dev (development branch only) ────────────────────────
# Runs after image build on every push to development. Updates
# values-dev.yaml with the SHA image tag so ArgoCD picks up the new build.
deploy-dev:
name: Deploy to dev
needs: image
if: github.ref == 'refs/heads/development'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Update values-dev.yaml on development branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
IMAGE_TAG="sha-${GITHUB_SHA::7}"
FILE_PATH="charts/bindery/values-dev.yaml"
DEPLOY_BRANCH="auto/dev-deploy-${GITHUB_SHA::7}"
DEV_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/git/refs/heads/development" --jq .object.sha)
gh api "repos/${GITHUB_REPOSITORY}/git/refs" \
-f ref="refs/heads/${DEPLOY_BRANCH}" \
-f sha="${DEV_SHA}"
FILE_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/contents/${FILE_PATH}?ref=${DEPLOY_BRANCH}" --jq .sha 2>/dev/null || echo "")
CONTENT=$(printf 'image:\n tag: "%s"\n' "${IMAGE_TAG}" | base64 -w0)
if [ -n "$FILE_SHA" ]; then
gh api "repos/${GITHUB_REPOSITORY}/contents/${FILE_PATH}" \
-X PUT \
-f message="chore(deploy-dev): update to ${IMAGE_TAG} [skip ci]" \
-f content="${CONTENT}" \
-f sha="${FILE_SHA}" \
-f branch="${DEPLOY_BRANCH}"
else
gh api "repos/${GITHUB_REPOSITORY}/contents/${FILE_PATH}" \
-X PUT \
-f message="chore(deploy-dev): create values-dev.yaml for ${IMAGE_TAG} [skip ci]" \
-f content="${CONTENT}" \
-f branch="${DEPLOY_BRANCH}"
fi
# --repo is required because this job does not check out the
# repository, so gh has no local git context to infer the target
# from. Without it, `gh pr create` exits 1 with "not a git
# repository: .git" and the 2>/dev/null mask hides that error;
# the subsequent `gh pr merge` then fails for the same reason
# (and orphans the auto/dev-deploy-<sha> branch with the right
# content but no PR). The deploy-prod job below already has the
# --repo flag; this brings deploy-dev in line.
gh pr create \
--repo "${GITHUB_REPOSITORY}" \
--head "${DEPLOY_BRANCH}" \
--base development \
--title "chore(deploy-dev): update to ${IMAGE_TAG}" \
--body "Automated dev deploy: image tag \`${IMAGE_TAG}\`." 2>/dev/null || true
gh pr merge "${DEPLOY_BRANCH}" --repo "${GITHUB_REPOSITORY}" --squash --admin --delete-branch
echo "bindery-dev image tag updated to ${IMAGE_TAG}"
- name: Trigger ArgoCD dev refresh
env:
ARGOCD_SERVER: ${{ secrets.ARGOCD_SERVER }}
ARGOCD_TOKEN: ${{ secrets.ARGOCD_TOKEN }}
run: |
if [ -z "${ARGOCD_SERVER:-}" ] || [ -z "${ARGOCD_TOKEN:-}" ]; then
echo "::warning::ARGOCD_SERVER or ARGOCD_TOKEN not configured — Argo will pick up changes on next poll"
exit 0
fi
curl -sf -X POST \
-H "Authorization: Bearer ${ARGOCD_TOKEN}" \
"${ARGOCD_SERVER}/api/v1/applications/bindery-dev?refresh=hard" \
&& echo "ArgoCD bindery-dev refresh triggered" \
|| echo "::warning::ArgoCD dev refresh failed — Argo will pick up changes on next poll"
# ── GoReleaser — runs after test passes, tag pushes only ───────────────────
goreleaser:
needs: test
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.25.10"
cache: true
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci --prefix web
- run: npm run build --prefix web
- name: Populate embedded frontend
run: rm -rf internal/webui/dist && cp -r web/dist internal/webui/dist && touch internal/webui/dist/.gitkeep
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${GITHUB_REF_NAME}"
NOTES=$(awk -v ver="## [${VERSION}]" '
index($0, ver) == 1 { found=1; next }
found && index($0, "## ") == 1 { exit }
found { print }
' CHANGELOG.md)
if [ -z "$NOTES" ]; then
echo "::error::No CHANGELOG section found for ${VERSION}. Add '## [${VERSION}]' to CHANGELOG.md before tagging."
exit 1
fi
TITLE=$(printf '%s' "$NOTES" | awk 'NF { print; exit }' | sed 's/^### //')
RELEASE_TITLE="${VERSION} — ${TITLE}"
if gh release view "$VERSION" &>/dev/null; then
echo "Release $VERSION already exists — updating notes."
printf '%s' "$NOTES" | gh release edit "$VERSION" --title "$RELEASE_TITLE" --notes-file -
else
printf '%s' "$NOTES" | gh release create "$VERSION" --title "$RELEASE_TITLE" --notes-file -
fi
- name: Install Syft
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0
- name: GoReleaser (release binaries)
uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7.2.1
with:
version: "~> v2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Bump bindery-ping LATEST_VERSION
env:
KUBECONFIG_B64: ${{ secrets.HETZ1_KUBECONFIG }}
VERSION: ${{ github.ref_name }}
run: |
[ -z "$KUBECONFIG_B64" ] && exit 0
echo "$KUBECONFIG_B64" | base64 -d > /tmp/kc
KUBECONFIG=/tmp/kc kubectl -n bindery-ping set env deployment/bindery-ping \
LATEST_VERSION="$VERSION"
rm -f /tmp/kc
# Pinned ABS compatibility suite. Runs self-contained against the
# repository's seeded Phase 6 fixture harness and can optionally target a
# real pinned ABS instance when the env vars below are provided.
abs-contract:
name: ABS contract
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.25.10"
cache: true
- name: Run ABS contract suite
env:
BINDERY_ABS_CONTRACT_BASE_URL: ${{ secrets.BINDERY_ABS_CONTRACT_BASE_URL }}
BINDERY_ABS_CONTRACT_API_KEY: ${{ secrets.BINDERY_ABS_CONTRACT_API_KEY }}
BINDERY_ABS_CONTRACT_LIMITED_API_KEY: ${{ secrets.BINDERY_ABS_CONTRACT_LIMITED_API_KEY }}
BINDERY_ABS_CONTRACT_LIBRARY_ID: ${{ secrets.BINDERY_ABS_CONTRACT_LIBRARY_ID }}
run: |
make abs-contract
# ── Pre-deploy smoke — sanity check dev before promoting to prod ───────────
predeploy-smoke:
name: Pre-deploy smoke
needs: image
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: "1.25.10"
cache: true
- name: Wait for ArgoCD dev to sync
env:
ARGOCD_SERVER: ${{ secrets.ARGOCD_SERVER }}
ARGOCD_TOKEN: ${{ secrets.ARGOCD_TOKEN }}
run: |
if [[ -z "$ARGOCD_SERVER" || -z "$ARGOCD_TOKEN" ]]; then
echo "ArgoCD secrets not set — skipping wait"
exit 0
fi
echo "Waiting for ArgoCD app 'bindery-dev' to be Healthy+Synced..."
for i in $(seq 1 24); do
# jq avoids a curl|python3 pipe flagged by OpenSSF Scorecard downloadThenRun
STATUS=$(curl -sf -H "Authorization: Bearer $ARGOCD_TOKEN" \
"$ARGOCD_SERVER/api/v1/applications/bindery-dev" 2>/dev/null | \
jq -r '"\(.status.health.status) \(.status.sync.status)"' 2>/dev/null || echo "unknown unknown")
echo " attempt $i: $STATUS"
[[ "$STATUS" == "Healthy Synced" ]] && echo "ArgoCD dev sync complete" && exit 0
sleep 10
done
echo "Timed out waiting for ArgoCD dev sync"
exit 1
- name: Run pre-deploy smoke tests
env:
BINDERY_URL: https://bindery.autonomy.ninja
BINDERY_API_KEY: ${{ secrets.BINDERY_SMOKE_API_KEY }}
run: |
if ! curl -sf --max-time 5 "$BINDERY_URL/api/v1/health" > /dev/null 2>&1; then
echo "::warning::Live instance unreachable from runner — skipping pre-deploy smoke"
exit 0
fi
make predeploy-smoke
# ── Deploy to prod (bindery) — tag push, after image + goreleaser + smoke ──
# Updates values.yaml on main; ArgoCD prod app (bindery) picks it up.
deploy-prod:
name: Deploy to prod
needs: [image, goreleaser, predeploy-smoke]
if: startsWith(github.ref, 'refs/tags/v') && needs.goreleaser.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Update values.yaml on main
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
IMAGE_TAG="${GITHUB_REF_NAME#v}"
FILE_PATH="charts/bindery/values.yaml"
DEPLOY_BRANCH="auto/prod-deploy-${IMAGE_TAG}"
MAIN_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/git/refs/heads/main" --jq .object.sha)
gh api "repos/${GITHUB_REPOSITORY}/git/refs" \
-f ref="refs/heads/${DEPLOY_BRANCH}" \
-f sha="${MAIN_SHA}"
FILE_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/contents/${FILE_PATH}?ref=${DEPLOY_BRANCH}" --jq .sha)
CONTENT=$(gh api "repos/${GITHUB_REPOSITORY}/contents/${FILE_PATH}?ref=${DEPLOY_BRANCH}" \
--jq '.content' | base64 -d | sed "s|tag:.*|tag: \"${IMAGE_TAG}\"|" | base64 -w0)
gh api "repos/${GITHUB_REPOSITORY}/contents/${FILE_PATH}" \
-X PUT \
-f message="chore(deploy): promote bindery to ${IMAGE_TAG} [skip ci]" \
-f content="${CONTENT}" \
-f sha="${FILE_SHA}" \
-f branch="${DEPLOY_BRANCH}"
gh pr create \
--repo "${GITHUB_REPOSITORY}" \
--head "${DEPLOY_BRANCH}" \
--base main \
--title "chore(deploy): promote bindery to ${GITHUB_REF_NAME}" \
--body "Automated prod deploy: image tag \`${IMAGE_TAG}\`." 2>/dev/null || true
gh pr merge "${DEPLOY_BRANCH}" --repo "${GITHUB_REPOSITORY}" --squash --admin --delete-branch
echo "bindery (prod) image tag updated to ${IMAGE_TAG}"
- name: Trigger ArgoCD prod refresh
env:
ARGOCD_SERVER: ${{ secrets.ARGOCD_SERVER }}
ARGOCD_TOKEN: ${{ secrets.ARGOCD_TOKEN }}
run: |
if [ -z "${ARGOCD_SERVER:-}" ] || [ -z "${ARGOCD_TOKEN:-}" ]; then
echo "::warning::ARGOCD_SERVER or ARGOCD_TOKEN not configured — Argo will pick up changes on next poll"
exit 0
fi
curl -sf -X POST \
-H "Authorization: Bearer ${ARGOCD_TOKEN}" \
"${ARGOCD_SERVER}/api/v1/applications/bindery?refresh=hard" \
&& echo "ArgoCD bindery (prod) refresh triggered" \
|| echo "::warning::ArgoCD prod refresh failed — Argo will pick up changes on next poll"
# ── Discord release announcement ───────────────────────────────────────────
notify-discord:
name: Notify Discord
needs: [goreleaser, image]
if: startsWith(github.ref, 'refs/tags/v') && needs.goreleaser.result == 'success'
runs-on: ubuntu-latest
# A broken/expired webhook should never fail the release workflow.
continue-on-error: true
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Post release announcement
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
VERSION: ${{ github.ref_name }}
run: |
PAYLOAD=$(python3 -c "
import json, re, os, sys
version = os.environ['VERSION']
with open('CHANGELOG.md') as f:
content = f.read()
pattern = r'## \[' + re.escape(version) + r'\][^\n]*\n(.*?)(?=\n## \[|\Z)'
m = re.search(pattern, content, re.DOTALL)
notes = m.group(1).strip() if m else ''
if len(notes) > 900:
notes = notes[:900].rsplit('\n', 1)[0] + '\n...'
body = '**Bindery ' + version + '** is out!'
if notes:
body += '\n\n' + notes
body += '\n\n<https://github.com/vavallee/bindery/releases/tag/' + version + '>'
print(json.dumps({'content': body}))
")
HTTP=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Content-Type: application/json" \
--data-binary "$PAYLOAD" \
"$DISCORD_WEBHOOK")
if [ "$HTTP" = "204" ]; then
echo "Discord notified for ${VERSION}"
else
echo "::warning::Discord webhook returned HTTP ${HTTP} — check DISCORD_RELEASE_WEBHOOK secret"
exit 1
fi