Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -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
18 changes: 17 additions & 1 deletion depot/service/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
Expand All @@ -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").
Expand Down
119 changes: 119 additions & 0 deletions scripts/release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
printf 'Usage: %s <version>\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."
2 changes: 1 addition & 1 deletion web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 44 additions & 6 deletions web/src/pages/AttributionDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,27 @@ 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 (
<Card>
<CardContent className="py-4">
<p className="text-xs text-muted-foreground">{label}</p>
{loading ? (
<Skeleton className="mt-1.5 h-6 w-20" />
) : (
<p className="mt-1 text-xl font-semibold tabular-nums">{value}</p>
<p className="mt-1 text-xl font-semibold tabular-nums">
{error ? "Unavailable" : value}
</p>
)}
</CardContent>
</Card>
Expand Down Expand Up @@ -115,15 +127,37 @@ export default function AttributionDetailsPage({ kind }: { kind: AttributionKind
</Card>

<div className="mb-4 grid grid-cols-2 gap-3 lg:grid-cols-4">
<StatTile label="Files" value={String(stats?.file_count ?? 0)} loading={statsQuery.isLoading} />
<StatTile label="Storage used" value={formatBytes(stats?.total_bytes ?? 0)} loading={statsQuery.isLoading} />
<StatTile label="Buckets" value={String(stats?.bucket_count ?? 0)} loading={statsQuery.isLoading} />
<StatTile label="Public files" value={String(stats?.public_files ?? 0)} loading={statsQuery.isLoading} />
<StatTile
label="Files"
value={String(stats?.file_count ?? 0)}
loading={statsQuery.isLoading}
error={statsQuery.isError}
/>
<StatTile
label="Storage used"
value={formatBytes(stats?.total_bytes ?? 0)}
loading={statsQuery.isLoading}
error={statsQuery.isError}
/>
<StatTile
label="Buckets"
value={String(stats?.bucket_count ?? 0)}
loading={statsQuery.isLoading}
error={statsQuery.isError}
/>
<StatTile
label="Public files"
value={String(stats?.public_files ?? 0)}
loading={statsQuery.isLoading}
error={statsQuery.isError}
/>
</div>

<p className="mb-6 text-xs text-muted-foreground">
{statsQuery.isLoading
? "Loading upload history..."
: statsQuery.isError
? "Could not load attribution statistics."
: formatUploadRange(stats?.first_upload_at, stats?.last_upload_at)}
</p>

Expand All @@ -136,6 +170,10 @@ export default function AttributionDetailsPage({ kind }: { kind: AttributionKind
<CardContent className="space-y-3">
{statsQuery.isLoading ? (
Array.from({ length: 3 }).map((_, index) => <Skeleton key={index} className="h-10 w-full" />)
) : statsQuery.isError ? (
<p className="py-4 text-sm text-muted-foreground">
Could not load bucket statistics.
</p>
) : (stats?.buckets ?? []).length === 0 ? (
<p className="py-4 text-sm text-muted-foreground">No bucket activity.</p>
) : (
Expand Down
Loading