From 5d96bd508dae08c0d7c7048fe63f2f5e58ceb633 Mon Sep 17 00:00:00 2001 From: Bharat Kathi Date: Mon, 31 Aug 2026 18:21:57 -0700 Subject: [PATCH 1/2] fix: return attribution statistics --- depot/service/stats.go | 18 ++++++++- web/src/pages/AttributionDetailsPage.tsx | 50 +++++++++++++++++++++--- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/depot/service/stats.go b/depot/service/stats.go index 00bfd6e..b634df7 100644 --- a/depot/service/stats.go +++ b/depot/service/stats.go @@ -52,6 +52,15 @@ type AttributionStats struct { Buckets []BucketStats `json:"buckets"` } +type attributionTotals struct { + FileCount int64 + TotalBytes int64 + BucketCount int64 + PublicFiles int64 + FirstUploadAt *time.Time + LastUploadAt *time.Time +} + type ActivityPoint struct { Date string `json:"date"` Uploads int64 `json:"uploads"` @@ -128,6 +137,7 @@ func GetAttributionStats(bucketIDs []string, filter AttributionFilter) (Attribut return result.Where("created_by_client_id = ?", filter.ClientID) } + totals := attributionTotals{} if err := query(). Select(` count(*) AS file_count, @@ -137,9 +147,15 @@ func GetAttributionStats(bucketIDs []string, filter AttributionFilter) (Attribut min(created_at) AS first_upload_at, max(created_at) AS last_upload_at `). - Scan(&stats).Error; err != nil { + Scan(&totals).Error; err != nil { return AttributionStats{}, fmt.Errorf("failed to compute attribution totals: %w", err) } + stats.FileCount = totals.FileCount + stats.TotalBytes = totals.TotalBytes + stats.BucketCount = totals.BucketCount + stats.PublicFiles = totals.PublicFiles + stats.FirstUploadAt = totals.FirstUploadAt + stats.LastUploadAt = totals.LastUploadAt if err := query(). Select("bucket_id, bucket_name, count(*) as file_count, coalesce(sum(size_bytes), 0) as total_bytes"). diff --git a/web/src/pages/AttributionDetailsPage.tsx b/web/src/pages/AttributionDetailsPage.tsx index 18b4d88..444d586 100644 --- a/web/src/pages/AttributionDetailsPage.tsx +++ b/web/src/pages/AttributionDetailsPage.tsx @@ -24,7 +24,17 @@ const PAGE_SIZE = 50 type AttributionKind = "uploader" | "application" -function StatTile({ label, value, loading }: { label: string; value: string; loading: boolean }) { +function StatTile({ + label, + value, + loading, + error, +}: { + label: string + value: string + loading: boolean + error: boolean +}) { return ( @@ -32,7 +42,9 @@ function StatTile({ label, value, loading }: { label: string; value: string; loa {loading ? ( ) : ( -

{value}

+

+ {error ? "Unavailable" : value} +

)}
@@ -115,15 +127,37 @@ export default function AttributionDetailsPage({ kind }: { kind: AttributionKind
- - - - + + + +

{statsQuery.isLoading ? "Loading upload history..." + : statsQuery.isError + ? "Could not load attribution statistics." : formatUploadRange(stats?.first_upload_at, stats?.last_upload_at)}

@@ -136,6 +170,10 @@ export default function AttributionDetailsPage({ kind }: { kind: AttributionKind {statsQuery.isLoading ? ( Array.from({ length: 3 }).map((_, index) => ) + ) : statsQuery.isError ? ( +

+ Could not load bucket statistics. +

) : (stats?.buckets ?? []).length === 0 ? (

No bucket activity.

) : ( From fa4e774a716b9281f5428ee6b121c5a69a928365 Mon Sep 17 00:00:00 2001 From: Bharat Kathi Date: Tue, 1 Sep 2026 18:18:09 -0700 Subject: [PATCH 2/2] ci: add Depot release and deployment automation --- .github/workflows/deploy.yml | 142 +++++++++++++++++++++++++++++++++++ scripts/release.sh | 119 +++++++++++++++++++++++++++++ web/Dockerfile | 2 +- 3 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy.yml create mode 100755 scripts/release.sh diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..07aa790 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,142 @@ +name: deploy +run-name: Open infrastructure PR for ${{ github.event.release.tag_name }} + +on: + release: + types: [published] + +env: + INFRA_REPO: Gaucho-Racing/infrastructure + KUSTOMIZATION: infra/kubernetes/gr-foundry/manifests/depot/kustomization.yaml + SERVICES: "server web" + +jobs: + infra-pr: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout Depot + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Pull secrets + id: vault + uses: Gaucho-Racing/vault-pull-secrets@v1 + with: + secrets: infrastructure.pr_token + + - name: Checkout infrastructure + uses: actions/checkout@v4 + with: + repository: ${{ env.INFRA_REPO }} + token: ${{ fromJSON(steps.vault.outputs.secrets_json).PR_TOKEN }} + path: infra + + - name: Resolve versions + id: versions + run: | + NEW_TAG="${{ github.event.release.tag_name }}" + NEW="${NEW_TAG#v}" + OLD="$(yq '.images[] | select(.name == "ghcr.io/gaucho-racing/depot-server") | .newTag' "$KUSTOMIZATION")" + echo "new_tag=$NEW_TAG" >> "$GITHUB_OUTPUT" + echo "new=$NEW" >> "$GITHUB_OUTPUT" + echo "old=$OLD" >> "$GITHUB_OUTPUT" + echo "old_tag=v$OLD" >> "$GITHUB_OUTPUT" + echo "short_sha=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_OUTPUT" + if [ "$NEW" = "$OLD" ]; then + echo "Infrastructure is already at $NEW." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Bump image tags + if: steps.versions.outputs.skip == 'false' + env: + NEW: ${{ steps.versions.outputs.new }} + run: | + awk -v new="$NEW" ' + $0 ~ "- name: ghcr.io/gaucho-racing/depot-(server|web)$" { indepot=1; print; next } + indepot==1 && $1=="newTag:" { sub(/newTag:[[:space:]]*.*/, "newTag: " new); indepot=0 } + { print } + ' "$KUSTOMIZATION" > "$KUSTOMIZATION.tmp" && mv "$KUSTOMIZATION.tmp" "$KUSTOMIZATION" + git -C infra diff -- kubernetes/gr-foundry/manifests/depot/kustomization.yaml + + - name: Build changelog + if: steps.versions.outputs.skip == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEPOT_REPO: ${{ github.repository }} + NEW_TAG: ${{ steps.versions.outputs.new_tag }} + OLD_TAG: ${{ steps.versions.outputs.old_tag }} + run: | + emit_commits() { + for sha in $(git rev-list --no-merges "$@"); do + short="$(git rev-parse --short "$sha")" + subject="$(git show -s --format=%s "$sha" | sed -E 's/ \(#[0-9]+\)$//')" + login="$(gh api "repos/$DEPOT_REPO/commits/$sha" --jq '.author.login // empty' 2>/dev/null || true)" + pr="$(gh api "repos/$DEPOT_REPO/commits/$sha/pulls" --jq '.[0].number // empty' 2>/dev/null || true)" + line="- \`$short\` $subject" + [ -n "$login" ] && line="$line - @$login" + [ -n "$pr" ] && line="$line ([#$pr](https://github.com/$DEPOT_REPO/pull/$pr))" + echo "$line" + done + } + { + echo "Bumps the Depot service images to **${NEW_TAG#v}**:" + echo + for service in $SERVICES; do echo "- \`depot-$service\` → \`${NEW_TAG#v}\`"; done + echo + echo "## Changes since ${OLD_TAG}" + echo + if git rev-parse "$OLD_TAG" >/dev/null 2>&1; then + COMMITS="$(emit_commits "${OLD_TAG}..${NEW_TAG}")" + if [ -n "$COMMITS" ]; then echo "$COMMITS"; else echo "_No non-merge commits between ${OLD_TAG} and ${NEW_TAG}._"; fi + else + echo "_Previous tag ${OLD_TAG} not found in history; showing the latest commits instead._" + echo + emit_commits -20 "$NEW_TAG" + fi + echo + echo "**[View all changes](https://github.com/${DEPOT_REPO}/compare/${OLD_TAG}...${NEW_TAG})**" + echo + echo "_Opened automatically by the deploy workflow on release ${NEW_TAG}._" + } > "$RUNNER_TEMP/pr-body.md" + cat "$RUNNER_TEMP/pr-body.md" + + - name: Create pull request + if: steps.versions.outputs.skip == 'false' + working-directory: infra + env: + GH_TOKEN: ${{ fromJSON(steps.vault.outputs.secrets_json).PR_TOKEN }} + NEW: ${{ steps.versions.outputs.new }} + NEW_TAG: ${{ steps.versions.outputs.new_tag }} + SHORT_SHA: ${{ steps.versions.outputs.short_sha }} + RELEASE_AUTHOR: ${{ github.event.release.author.login }} + run: | + BRANCH="bot/bump-depot-${NEW}" + git config user.name "GR Admin" + git config user.email "team@gauchoracing.com" + git switch -C "$BRANCH" + git commit -am "Deploy: depot to ${NEW_TAG} (${SHORT_SHA})" + git push -f -u origin "$BRANCH" + PR="$(gh pr list --repo "$INFRA_REPO" --head "$BRANCH" --state open --json number -q '.[0].number')" + if [ -n "$PR" ]; then + echo "PR #$PR already open for $BRANCH; pushed the updated branch." + else + gh pr create \ + --repo "$INFRA_REPO" \ + --base main \ + --head "$BRANCH" \ + --title "Deploy: depot to ${NEW_TAG} (${SHORT_SHA})" \ + --body-file "$RUNNER_TEMP/pr-body.md" + PR="$(gh pr list --repo "$INFRA_REPO" --head "$BRANCH" --state open --json number -q '.[0].number')" + fi + if [ -n "$PR" ] && [ -n "$RELEASE_AUTHOR" ]; then + gh pr edit "$PR" --repo "$INFRA_REPO" --add-reviewer "$RELEASE_AUTHOR" \ + || echo "Could not request review from @$RELEASE_AUTHOR." + fi diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..72fc97b --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + printf 'Usage: %s \n\nExamples:\n %s 0.1.0\n %s\n' "$0" "$0" "$0" +} + +while getopts ":h" option; do + case "$option" in + h) usage; exit 0 ;; + *) usage; exit 1 ;; + esac +done +shift $((OPTIND - 1)) + +INPUT="${1:-}" + +for command in gh git; do + if ! command -v "$command" >/dev/null 2>&1; then + echo "Error: $command is required" + exit 1 + fi +done + +if ! gh auth status >/dev/null 2>&1; then + echo "Error: gh must be authenticated" + exit 1 +fi + +BRANCH="$(git rev-parse --abbrev-ref HEAD)" +if [[ "$BRANCH" != "main" ]]; then + echo "Error: must be on main branch (currently on $BRANCH)" + exit 1 +fi + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +if [[ -n "$(git status --porcelain)" ]]; then + echo "Error: working tree must be clean" + exit 1 +fi + +git fetch origin main --tags --quiet +LOCAL="$(git rev-parse HEAD)" +REMOTE="$(git rev-parse origin/main)" +if [[ "$LOCAL" != "$REMOTE" ]]; then + echo "Error: local main is not up to date with origin/main" + echo " local: $LOCAL" + echo " remote: $REMOTE" + exit 1 +fi + +PREVIOUS="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -n1)" + +if [[ -z "$INPUT" ]]; then + echo + if [[ -n "$PREVIOUS" ]]; then + echo "Current release: $PREVIOUS" + else + echo "Current release: (none)" + fi + echo + read -rp "Enter new version: " INPUT +fi + +INPUT="${INPUT#v}" +if [[ ! "$INPUT" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: version must be a valid semver (e.g. 0.1.0)" + exit 1 +fi + +SEMVER="$INPUT" +TAG="v$INPUT" +IMAGES=("depot-server" "depot-web") + +if git tag -l "$TAG" | grep -q "^${TAG}$"; then + echo "Error: tag $TAG already exists" + exit 1 +fi + +if gh release view "$TAG" >/dev/null 2>&1; then + echo "Error: release $TAG already exists" + exit 1 +fi + +echo +echo "=== Release Summary ===" +echo " Version: $TAG" +echo " Commit: $(git rev-parse --short HEAD)" +echo " Branch: main" +echo +echo " File to update:" +echo " depot/config/config.go" +echo +echo " Docker images that will be tagged:" +for image in "${IMAGES[@]}"; do + echo " ghcr.io/gaucho-racing/${image}:${SEMVER}" +done +echo +read -rp "Proceed? (y/N) " CONFIRMATION +if [[ "$CONFIRMATION" != "y" && "$CONFIRMATION" != "Y" ]]; then + echo "Aborted." + exit 0 +fi + +sed -i '' "s/const Version = \".*\"/const Version = \"${SEMVER}\"/" "$REPO_ROOT/depot/config/config.go" + +git add depot/config/config.go +git commit --allow-empty -m "release: depot $TAG" +git push origin main + +gh release create "$TAG" \ + --target main \ + --title "$TAG" \ + --generate-notes + +echo +echo "Done. $TAG released. Workflows will publish images and open the deployment PR shortly." diff --git a/web/Dockerfile b/web/Dockerfile index 87aaa48..d42656e 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -9,7 +9,7 @@ COPY . ./ ARG VITE_API_URL="" ARG VITE_SENTINEL_URL="https://sso.gauchoracing.com" -ARG VITE_SENTINEL_CLIENT_ID="" +ARG VITE_SENTINEL_CLIENT_ID="Z5cwPi05LlBv" ENV VITE_API_URL=$VITE_API_URL ENV VITE_SENTINEL_URL=$VITE_SENTINEL_URL