Skip to content

Commit e27262e

Browse files
committed
first pass at update status
1 parent ee38e56 commit e27262e

File tree

23 files changed

+2602
-1189
lines changed

23 files changed

+2602
-1189
lines changed

.github/workflows/deploy-docs.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Deploy documentation site to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
# Allows you to run this workflow manually from the Actions tab
7+
workflow_dispatch:
8+
9+
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
10+
permissions:
11+
contents: read
12+
pages: write
13+
id-token: write
14+
15+
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
16+
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
17+
concurrency:
18+
group: pages
19+
cancel-in-progress: false
20+
21+
jobs:
22+
# Build job
23+
build:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Checkout
27+
uses: actions/checkout@v5
28+
with:
29+
fetch-depth: 0 # Not needed if lastUpdated is not enabled
30+
- name: Setup Node
31+
uses: actions/setup-node@v6
32+
with:
33+
node-version: 24
34+
cache: npm
35+
- name: Setup Pages
36+
uses: actions/configure-pages@v4
37+
- name: Install dependencies
38+
working-directory: docs
39+
run: npm ci
40+
- name: Build with VitePress
41+
working-directory: docs
42+
run: npm run build
43+
- name: Upload artifact
44+
uses: actions/upload-pages-artifact@v3
45+
with:
46+
path: docs/.vitepress/dist
47+
48+
# Deployment job
49+
deploy:
50+
environment:
51+
name: github-pages
52+
url: ${{ steps.deployment.outputs.page_url }}
53+
needs: build
54+
runs-on: ubuntu-latest
55+
name: Deploy
56+
steps:
57+
- name: Deploy to GitHub Pages
58+
id: deployment
59+
uses: actions/deploy-pages@v4

.github/workflows/edge.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Edge (main -> edge image)
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions:
9+
packages: write
10+
11+
jobs:
12+
build-and-push-edge:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Set up QEMU
19+
uses: docker/setup-qemu-action@v3
20+
21+
- name: Set up Docker Buildx
22+
uses: docker/setup-buildx-action@v3
23+
24+
- name: Log in to GHCR
25+
uses: docker/login-action@v3
26+
with:
27+
registry: ghcr.io
28+
username: ${{ github.actor }}
29+
password: ${{ secrets.GITHUB_TOKEN }}
30+
31+
- name: Extract build metadata
32+
id: meta
33+
run: |
34+
echo "commit_sha=${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT"
35+
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT"
36+
37+
- name: Build & push edge image
38+
uses: docker/build-push-action@v6
39+
with:
40+
context: .
41+
file: docker/Dockerfile.prod
42+
push: true
43+
platforms: linux/amd64,linux/arm64
44+
build-args: |
45+
ARRFLIX_VERSION=edge
46+
ARRFLIX_COMMIT=${{ steps.meta.outputs.commit_sha }}
47+
ARRFLIX_BUILD_DATE=${{ steps.meta.outputs.build_date }}
48+
tags: |
49+
ghcr.io/${{ github.repository_owner }}/arrflix:edge
50+
ghcr.io/${{ github.repository_owner }}/arrflix:sha-${{ github.sha }}
51+
labels: |
52+
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
53+
org.opencontainers.image.revision=${{ github.sha }}

.github/workflows/release.yml

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
name: Release (tag -> image + GH Release)
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
permissions:
9+
contents: write
10+
packages: write
11+
12+
jobs:
13+
build-and-push:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Set up QEMU
20+
uses: docker/setup-qemu-action@v3
21+
22+
- name: Set up Docker Buildx
23+
uses: docker/setup-buildx-action@v3
24+
25+
- name: Log in to GHCR
26+
uses: docker/login-action@v3
27+
with:
28+
registry: ghcr.io
29+
username: ${{ github.actor }}
30+
password: ${{ secrets.GITHUB_TOKEN }}
31+
32+
- name: Extract version + prerelease flag
33+
id: ver
34+
shell: bash
35+
run: |
36+
VERSION="${GITHUB_REF_NAME#v}"
37+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
38+
39+
if [[ "$VERSION" == *"-"* ]]; then
40+
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
41+
else
42+
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
43+
fi
44+
45+
- name: Extract build metadata
46+
id: meta
47+
run: |
48+
echo "commit_sha=${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT"
49+
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT"
50+
51+
# Push immutable tags always: version + sha
52+
- name: Build & push image (version + sha)
53+
uses: docker/build-push-action@v6
54+
with:
55+
context: .
56+
file: docker/Dockerfile.prod
57+
push: true
58+
platforms: linux/amd64,linux/arm64
59+
build-args: |
60+
ARRFLIX_VERSION=${{ steps.ver.outputs.version }}
61+
ARRFLIX_COMMIT=${{ steps.meta.outputs.commit_sha }}
62+
ARRFLIX_BUILD_DATE=${{ steps.meta.outputs.build_date }}
63+
tags: |
64+
ghcr.io/${{ github.repository_owner }}/arrflix:${{ steps.ver.outputs.version }}
65+
ghcr.io/${{ github.repository_owner }}/arrflix:sha-${{ github.sha }}
66+
labels: |
67+
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
68+
org.opencontainers.image.revision=${{ github.sha }}
69+
org.opencontainers.image.version=${{ steps.ver.outputs.version }}
70+
71+
# Move :latest ONLY for stable releases (no prerelease suffix)
72+
- name: Update :latest (stable only)
73+
if: steps.ver.outputs.is_prerelease == 'false'
74+
shell: bash
75+
run: |
76+
IMAGE="ghcr.io/${{ github.repository_owner }}/arrflix"
77+
VERSION="${{ steps.ver.outputs.version }}"
78+
79+
# Retag the already-pushed multi-arch manifest (no rebuild)
80+
docker buildx imagetools create \
81+
-t "${IMAGE}:latest" \
82+
"${IMAGE}:${VERSION}"
83+
84+
- name: Create GitHub Release (changelog)
85+
uses: softprops/action-gh-release@v2
86+
with:
87+
tag_name: ${{ github.ref_name }}
88+
name: Arrflix ${{ github.ref_name }}
89+
generate_release_notes: true
90+
prerelease: ${{ steps.ver.outputs.is_prerelease }}

CLAUDE.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Arrflix is a self-hosted media management platform that unifies the best parts o
1212
- **Frontend**: Vue 3 + TypeScript (Vite, Vue Router, Pinia, TanStack Query)
1313
- **Database**: PostgreSQL
1414
- **API**: RESTful with auto-generated OpenAPI/Swagger docs
15-
- **Deployment**: Docker with supervisord orchestrating services
15+
- **Deployment**: Docker with s6-overlay process manager
1616

1717
## Development Setup
1818

@@ -181,3 +181,32 @@ cd backend && go run cmd/quality-test/main.go
181181
# Generate password hash for user creation
182182
cd backend && go run cmd/password/main.go
183183
```
184+
185+
## Version and Update System
186+
187+
Arrflix includes a built-in version tracking and update check system:
188+
189+
**Build Metadata:**
190+
- Version information is injected at Docker build time via build args
191+
- Environment variables: `ARRFLIX_VERSION`, `ARRFLIX_COMMIT`, `ARRFLIX_BUILD_DATE`, `PROWLARR_VERSION`
192+
- Dev builds default to version `dev` with no update checks
193+
- Edge builds (from main branch) compare commit SHAs
194+
- Stable releases use semantic versioning and compare against GitHub releases
195+
196+
**API Endpoints:**
197+
- `GET /api/v1/version` - Returns current build information
198+
- `GET /api/v1/update` - Checks GitHub for available updates (cached 15 minutes)
199+
200+
**Implementation:**
201+
- `backend/internal/versioninfo/` - Reads environment variables
202+
- `backend/internal/github/` - GitHub API client
203+
- `backend/internal/semver/` - Semantic version comparison
204+
- `backend/internal/service/version.go` - Update check logic with caching
205+
- `web/src/components/settings/VersionCard.vue` - UI component
206+
207+
**Update Logic:**
208+
- Dev builds: Always show status "unknown"
209+
- Edge builds: Compare commit SHA with GitHub main HEAD
210+
- Stable releases: Compare semver with latest GitHub release, show release notes
211+
- Prereleases: Always show status "unknown"
212+
- GitHub API responses cached for 15 minutes using existing `api_cache` table

backend/internal/github/client.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"time"
10+
)
11+
12+
const (
13+
GitHubAPIBase = "https://api.github.com"
14+
UserAgent = "Arrflix"
15+
)
16+
17+
type Client struct {
18+
httpClient *http.Client
19+
owner string
20+
repo string
21+
}
22+
23+
// NewClient creates a new GitHub API client
24+
func NewClient(owner, repo string) *Client {
25+
return &Client{
26+
httpClient: &http.Client{Timeout: 10 * time.Second},
27+
owner: owner,
28+
repo: repo,
29+
}
30+
}
31+
32+
// Release represents a GitHub release
33+
type Release struct {
34+
TagName string `json:"tag_name"`
35+
Name string `json:"name"`
36+
Body string `json:"body"`
37+
HTMLURL string `json:"html_url"`
38+
PublishedAt time.Time `json:"published_at"`
39+
Prerelease bool `json:"prerelease"`
40+
}
41+
42+
// Commit represents a GitHub commit
43+
type Commit struct {
44+
SHA string `json:"sha"`
45+
Commit struct {
46+
Author struct {
47+
Date time.Time `json:"date"`
48+
} `json:"author"`
49+
} `json:"commit"`
50+
HTMLURL string `json:"html_url"`
51+
}
52+
53+
// GetLatestRelease fetches the latest stable release
54+
func (c *Client) GetLatestRelease(ctx context.Context) (*Release, error) {
55+
url := fmt.Sprintf("%s/repos/%s/%s/releases/latest", GitHubAPIBase, c.owner, c.repo)
56+
return doRequest[Release](ctx, c.httpClient, url)
57+
}
58+
59+
// GetMainHeadCommit fetches the latest commit on main branch
60+
func (c *Client) GetMainHeadCommit(ctx context.Context) (*Commit, error) {
61+
url := fmt.Sprintf("%s/repos/%s/%s/commits/main", GitHubAPIBase, c.owner, c.repo)
62+
return doRequest[Commit](ctx, c.httpClient, url)
63+
}
64+
65+
func doRequest[T any](ctx context.Context, client *http.Client, url string) (*T, error) {
66+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
req.Header.Set("User-Agent", UserAgent)
72+
req.Header.Set("Accept", "application/vnd.github+json")
73+
74+
resp, err := client.Do(req)
75+
if err != nil {
76+
return nil, err
77+
}
78+
defer resp.Body.Close()
79+
80+
if resp.StatusCode != http.StatusOK {
81+
body, _ := io.ReadAll(resp.Body)
82+
return nil, fmt.Errorf("github api error: %d %s", resp.StatusCode, string(body))
83+
}
84+
85+
var result T
86+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
87+
return nil, err
88+
}
89+
90+
return &result, nil
91+
}

0 commit comments

Comments
 (0)