Skip to content
Draft
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
20 changes: 10 additions & 10 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ jobs:
fail-fast: false
matrix:
include:
- name: desktop-repository-server-tunit
project: DesktopRepositoryServer.Tests/DesktopRepositoryServer.Tests.csproj
- name: repository-server-tunit
project: RepositoryServer.Tests/RepositoryServer.Tests.csproj

steps:
- name: Checkout
Expand All @@ -47,8 +47,8 @@ jobs:
fail-fast: false
matrix:
include:
- dockerfile: docker/DesktopRepositoryServer.Dockerfile
image: desktop-repository-server
- dockerfile: docker/RepositoryServer.Dockerfile
image: repository-server

steps:
- name: Checkout
Expand All @@ -72,7 +72,7 @@ jobs:
image: ${{ matrix.image }}
platforms: ${{ inputs.platforms || 'linux/amd64' }}
tag-prefix: ${{ inputs.tag-prefix || 'rn' }}

promote-image:
name: Promote Image (${{ matrix.image }})
needs: [build, tests]
Expand All @@ -82,8 +82,8 @@ jobs:
fail-fast: false
matrix:
include:
- image: desktop-repository-server
- image: repository-server

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -101,7 +101,7 @@ jobs:
image: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.image }}
run-tag: ${{ inputs.tag-prefix || 'rn' }}-${{ github.run_number }}-a${{ github.run_attempt }}
latest: ${{ inputs.latest || false }}

deploy-prod-workflow:
runs-on: ubuntu-latest
needs: promote-image
Expand All @@ -112,7 +112,7 @@ jobs:
uses: OpenShock/actions/repository-dispatch@21ca2d511bca92cf24fb502fa302f82700822584 # v1.1.1
with:
repo: openshock/kubernetes-cluster-gitops
event-type: update-desktop-repository-server-prod
event-type: update-repository-server-prod
client-payload: |
{"tag": "${{ inputs.tag-prefix || 'rn' }}-${{ github.run_number }}-a${{ github.run_attempt }}"}
token: ${{ secrets.GITOPS_PAT }}
token: ${{ secrets.GITOPS_PAT }}
20 changes: 10 additions & 10 deletions .github/workflows/ci-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ name: ci-tag

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/api
IMAGE_NAME: ${{ github.repository_owner }}/repository-server

jobs:

build-release:
runs-on: ubuntu-latest

steps:

- name: Checkout
Expand All @@ -40,33 +40,33 @@ jobs:
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/desktop-repository-server
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/repository-server
flavor: |
latest=false
tags: |
type=raw,value=latest,enable=${{ steps.latest-tag.outputs.tag == github.ref_name }}
type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}.{{minor}}.{{patch}}

- name: Build and push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: Dockerfile
file: docker/RepositoryServer.Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64


release:
runs-on: ubuntu-latest
needs: build-release

steps:
- name: Create release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
tag_name: ${{ github.ref_name }}
prerelease: ${{ contains(github.ref_name, '-rc') }}
prerelease: ${{ contains(github.ref_name, '-rc') }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
.idea
obj/
bin/
*.user
*.user
.claude/settings.local.json
141 changes: 141 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# OpenShock Repository Server

## Project Overview
Unified repository server for distributing OpenShock desktop app modules and firmware OTA updates. ASP.NET Core Web API with PostgreSQL/EF Core backend.

## Tech Stack
- .NET 10, ASP.NET Core (SlimBuilder)
- PostgreSQL via Npgsql + EF Core (with DbContext pooling)
- Serilog (console + Grafana Loki)
- OpenTelemetry (Prometheus exporter)
- API Versioning (Asp.Versioning) — URL-based: `/v1/`, `/v2/firmware/`
- FlexLabs.EntityFrameworkCore.Upsert for upserts
- MiniValidation for config validation
- Semver NuGet package for semver parsing
- OneOf for discriminated parser results
- TUnit for tests
- Scalar for API docs UI
- Docker (Alpine) with self-signed HTTPS cert generation

## Project Structure
```
RepositoryServer/ # Main web API project
AuthenticationHandlers/
AdminTokenAuthentication.cs # AdminToken scheme (static header compare)
GitHubOidcAuthentication.cs # JwtBearer + GitHub OIDC claim handling
# auto-upserts repositories rows on first use
Config/ # ApiConfig, DbConfig, RepoConfig, MetricsConfig,
# FirmwareConfig, FirmwareCiCdConfig, FirmwareAdvisoryConfig, DiscordConfig
Controllers/
OpenShockControllerBase.cs # Shared base controller
V1/ # Desktop module endpoints (V1)
RepoController.cs # GET /v1/ — desktop module manifest
AdminController.cs # /v1/admin/... — desktop module CRUD
CiCdController.cs # PUT /v1/cicd/modules/{id}/versions/{v} — desktop zip upload
V2/Firmware/ # Firmware V2 endpoints
ManifestController.cs # GET /v2/firmware/manifest (bootstrap payload)
LatestController.cs # GET /v2/firmware/latest/{channel}[/{boardId}?version=]
VersionsController.cs # GET /v2/firmware/versions[/{version}[/{boardId}]]
BoardsController.cs # GET /v2/firmware/boards
ChipsController.cs # GET /v2/firmware/chips
ReleasesController.cs # POST/PUT/DELETE /v2/firmware/releases/... (CI/CD ingestion)
Admin/ # AdminToken-protected CRUD (split by concern)
RepositoriesController.cs # GET, DELETE /v2/firmware/admin/repositories
VersionsAdminController.cs # DELETE /v2/firmware/admin/versions/{v}
BoardsAdminController.cs # PUT, PATCH, DELETE /v2/firmware/admin/boards/{id}
# + USB attach/detach
ChipsAdminController.cs # PUT, DELETE /v2/firmware/admin/chips/{id} + USB attach/detach
ReleasesAdminController.cs # PUT /v2/firmware/admin/releases/{id}/changelog (fix)
UsbDevicesController.cs # PUT/GET/DELETE /v2/firmware/admin/usb-devices
UsbSerialFiltersController.cs # PUT/GET/DELETE /v2/firmware/admin/usb-serial-filters
Enums/ # ReleaseChannel, FirmwareArtifactType, FirmwareChipArchitecture,
# ReleaseNoteSectionType, ReleaseStatus, RepositoryProvider
Errors/ # AuthResultError, ExceptionError
ExceptionHandler/ # Global IExceptionHandler + RequestInfo logging
Migrations/ # EF Core migrations
Models/ # Desktop module DTOs
Models/Firmware/ # Firmware DTOs
FirmwareManifestResponse, FirmwareReleaseDto, FirmwareVersionSummary,
FirmwareBoardDetailDto, FirmwareChipRefDto, FirmwareBoardReleaseResponseDto,
FirmwareSourceDto, FirmwareAdvisoryDto, FirmwareUsbDeviceDto,
FirmwareUsbSerialFilterDto (+ admin variant), RepositoryDto, VersionListResponse,
InitReleaseRequest (+ response), FixChangelogRequest, Upsert* requests
Problems/ # RFC 7807 problem details (OpenShockProblem base)
RepoServerDb/ # EF Core entities + RepoServerContext
SourceRepository, FirmwareVersion, FirmwareRelease, FirmwareArtifact, FirmwareReleaseNote,
FirmwareStagedArtifact, FirmwareStagedReleaseNote, FirmwareBoard, FirmwareChip,
UsbDevice, UsbSerialFilter, FirmwareChipUsbDevice, FirmwareBoardUsbDevice
Services/
IStorageService (+ BunnyCdn / Local / S3 impls)
IDiscordNotificationService + DiscordNotificationService (webhook, fire-and-forget)
StagedReleaseCleanupService # BackgroundService, PeriodicTimer 5 min, two TTLs
Utils/
ChangelogParser # Markdown → FirmwareReleaseNoteDto[] (OneOf-based)
SourceUrlBuilder # provider-aware commit/ref/run URL construction
FirmwareResponseMapper # shared release/artifact response projections
CacheControlAttribute # filter per spec §8
FirmwareArtifactFileNames, ByteArrayHexConverter, SemVersionConverter
Program.cs # App startup: schemes, DbContextPool, HttpClient,
# HostedService, storage, migrations, etc.

RepositoryServer.Tests/ # TUnit test project (references the main project)
Utils/
ChangelogParserTests
SourceUrlBuilderTests
docker/
RepositoryServer.Dockerfile
entrypoint.sh # Cert gen + `dotnet OpenShock.RepositoryServer.dll`
appsettings.Container.json
```

## Namespace
`OpenShock.RepositoryServer`

## Key Patterns
- **Auth**: Two schemes. `AdminTokenAuthentication` (header compare) for admin endpoints;
JwtBearer + `GitHubOidcAuthentication.Configure(...)` for CI/CD endpoints. The OIDC hook
validates a GitHub Actions OIDC JWT, then looks up (or inserts) a matching row in the
`repositories` table and attaches `AuthSchemas.CiCdClaims` to the principal.
No config allowlist — the DB is authoritative.
- **Error Handling**: `OpenShockProblem` base class → `ToObjectResult()` for RFC 7807
- **Config**: Bind root config to `ApiConfig`, validated with MiniValidator, registered as singleton
- **Background jobs**: `StagedReleaseCleanupService` (`BackgroundService` with `PeriodicTimer`)
periodically aborts expired staging/editing releases per `FirmwareConfig.StagedReleaseTtl`
and `EditingReleaseTtl`
- **DB Migrations**: `MigrationOpenShockContext` subclass for migration tooling, auto-applied on startup
- **JSON**: CamelCase, custom `SemVersionConverter` + `ByteArrayHexConverter`
- **Upsert**: FlexLabs `.Upsert().On().RunAsync()` pattern
- **Caching**: `CacheControlAttribute` action filter — public max-age on 2xx, no-store on errors
- **CORS**: Allow all origins/methods/headers
- **Metrics**: Prometheus at `/metrics`, restricted to private networks
- **Notifications**: Fire-and-forget Discord webhooks via `IDiscordNotificationService`.
Empty `Discord.WebhookUrls` → no-op.

## Database
- PostgreSQL (15+ required for `NULLS NOT DISTINCT` on `usb_serial_filters`), connection in `ApiConfig.Db.Conn`
- Shared table: `repositories` (PK: id, UNIQUE `(provider, owner, repo)`)
- Desktop tables: `modules` (PK: id), `versions` (composite PK: version+module, FK→modules CASCADE)
- Firmware tables: `firmware_chips`, `firmware_boards`, `firmware_versions`,
`firmware_artifacts`, `firmware_release_notes`, `firmware_releases`,
`firmware_staged_artifacts`, `firmware_staged_release_notes`
- USB catalog: `usb_devices`, `usb_serial_filters`, `firmware_chip_usb_devices`,
`firmware_board_usb_devices`
- Firmware enums: `release_channel`, `firmware_artifact_type`, `firmware_release_note_type`,
`firmware_chip_architecture`, `release_status`, `repository_provider`
- URL convention for firmware artifacts: `{CdnBaseUrl}/{version}/{boardId}/{artifactType}.bin`
- Source-traceability URLs (commit / ref / run) are built server-side per provider
(`Utils/SourceUrlBuilder`) and never stored

## CI/CD
- `ci-build.yml`: Test + build Docker + promote image + deploy prod (on master)
- `ci-tag.yml`: Release builds on semver tags, multi-arch (amd64+arm64)
- Image: `ghcr.io/openshock/repository-server`
- GitOps dispatch: `update-repository-server-prod` → `openshock/kubernetes-cluster-gitops`

## Commands
- Build: `dotnet build RepositoryServer/RepositoryServer.csproj`
- Test: `dotnet test --project RepositoryServer.Tests/RepositoryServer.Tests.csproj`
(Microsoft.Testing.Platform runner, opted in via `global.json`; the legacy positional
project path is rejected — use `--project`)
- Docker: `docker build -f docker/RepositoryServer.Dockerfile .`
- Migration: `dotnet ef migrations add <Name> --project RepositoryServer`
17 changes: 0 additions & 17 deletions DesktopRepositoryServer.Tests/DesktopRepositoryServer.Tests.csproj

This file was deleted.

10 changes: 0 additions & 10 deletions DesktopRepositoryServer.Tests/UnitTest1.cs

This file was deleted.

6 changes: 0 additions & 6 deletions DesktopRepositoryServer/AuthSchemas.cs

This file was deleted.

Loading