Skip to content

feat: Unified repository servers - #82

Merged
hhvrc merged 29 commits into
developfrom
feature/unified-repository-server
Aug 13, 2026
Merged

feat: Unified repository servers#82
hhvrc merged 29 commits into
developfrom
feature/unified-repository-server

Conversation

@hhvrc

@hhvrc hhvrc commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

No description provided.

hhvrc and others added 17 commits February 11, 2026 12:08
…support

Rename DesktopRepositoryServer to RepositoryServer with unified namespace
OpenShock.RepositoryServer. Reorganize controllers into V1 (backwards-compat
desktop endpoints at /v1/) and V2 (desktop at /v2/desktop/, firmware at
/v2/firmware/). Add full firmware repository with chips, boards, versions,
artifacts, and release notes backed by PostgreSQL enums and EF Core entities.
Includes public OTA endpoints and admin CRUD with token auth. Update Docker,
CI/CD workflows, and generate AddFirmwareTables migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add PUT /v2/firmware/admin/versions/{version}/boards/{board}/upload
  endpoint that accepts multipart binary uploads, computes SHA256,
  uploads to BunnyCDN via HTTP API, and stores artifact metadata
- Add CdnStorageService wrapping BunnyCDN Storage HTTP API
- Extend FirmwareConfig with CdnStorageUrl and CdnStorageApiKey
- Update LatestController to return all artifact types (not just Merged)
  and add optional ?board= query filter
- Unify FirmwareLatestResponse to use List<FirmwareArtifactDto> matching
  FirmwareVersionResponse shape
- Make artifacts optional in CreateFirmwareVersionRequest
- Extract shared GetArtifactFileName into FirmwareArtifactFileNames util
- Delete unused FirmwareBoardArtifact model

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…penshock/RepositoryServers into feature/unified-repository-server
Restores RepoServerDb/Module.cs, RepoServerDb/Version.cs and V1/CiCdController.cs
to their pre-firmware-rewrite state. The firmware work should not be modifying
desktop module code.
- Test fixture now injects config via UseSetting so values reach Program.cs
  before ApiConfig validation (ConfigureAppConfiguration overlays only apply
  at Build() in minimal hosting)
- Make RepoServerContext.MapEnums the single source of enum mappings with
  pinned DB names; drop the duplicate HasPostgresEnum block and map enums in
  the migration-tool path too
- Register only the pooled DbContext factory and derive scoped contexts from
  it; the AddDbContextPool + AddPooledDbContextFactory combo applied MapEnums
  twice and broke type mapping on first model use
- Order advisories by CLR severity rank in memory since MapEnum creates PG
  enum labels alphabetically
- Opt into the Microsoft.Testing.Platform dotnet test runner via global.json
  and update the documented test command
…erable transitives

- Generate the firmware/USB/repositories schema migration (enums, 14 new
  tables, unique constraints); desktop tables intentionally untouched
- Declare the usb_serial_filters unique (vid, pid) NULLS NOT DISTINCT index
  fluently via AreNullsDistinct(false) instead of the never-written raw-SQL
  migration the model comment referenced
- Test fixture now applies real migrations through MigrationOpenShockContext
  instead of EnsureCreated, so the suite validates the migration chain
- Enable CPM transitive pinning and pin Microsoft.OpenApi 2.7.5
  (GHSA-v5pm-xwqc-g5wc) and OpenTelemetry.Api 1.15.3 (GHSA-g94r-2vxg-569j)
- Replace deprecated HasCount(n) assertions with Count().IsEqualTo(n)
- Pass the Postgres image via the PostgreSqlBuilder constructor instead of
  the obsolete parameterless constructor + WithImage
C# mirrors of the ReleaseData/ChangeEntry/NoticeEntry/ReleaseNoteEntry/
Repository structs emitted by the OpenShock release-tool (schema_version 1).
Not consumed yet — ingestion wiring lands separately once the integration
point is decided.
…pository-server

# Conflicts:
#	.github/workflows/ci-build.yml
#	Directory.Packages.props
#	RepositoryServer/Controllers/V1/RepoController.cs
#	RepositoryServer/ExceptionHandler/ExceptionHandler.cs
#	RepositoryServer/appsettings.json
@hhvrc hhvrc self-assigned this Aug 4, 2026
Hubs can only know their board as a name — OPENSHOCK_FW_BOARD is the
PlatformIO env, compiled in at build time — but the single-board endpoints
were constrained to {boardId:guid}. A hub had no way to obtain that UUID, so
the route constraint rejected its request before the handler ever ran.

Make the name the public identifier everywhere: path segment, the boardId
field in responses, the boards map key, and the board segment of artifact
URLs and storage keys. UUIDs remain the primary key and FK target, and are
still accepted on public paths for admin tooling that holds one.

CI/CD ingestion follows the same rule, so workflows never need a name->UUID
lookup pass: InitReleaseRequest.Boards takes names, the upload route takes
{board}, and the release-incomplete / missing-artifact problems name the
board instead of emitting a UUID into CI logs.

Resolution goes through one chokepoint, FirmwareBoardLookup, which returns
the canonical stored name regardless of what the caller sent. That is where
board aliases plug in later: alternate names can map onto a canonical board
without touching storage layout, response shapes, or hubs. Renaming a board
in place is not supported and is documented as such — published versions are
immutable and their artifacts live under the name in force at publish time.

Board names are now validated as URL-safe, since they are interpolated into
storage keys unescaped; this notably rejects '/', which would otherwise let a
board name reshape the storage path.

Also fold the five copies of the CDN path interpolation into
FirmwareArtifactFileNames.BuildStoragePath/BuildUrl, and resolve the board
before the up-to-date check in LatestController so an unknown board reports
404 rather than being masked as 204 "no update needed".

Spec: correct §9.2, which told hubs to flash the 'merged' artifact. That is a
full-flash esptool image (bootloader at 0x1000, partition table at 0x8000,
app at 0x10000); writing it into an OTA app slot would put a bootloader image
where the app belongs. Hubs flash app + staticfs per-partition.
@hhvrc
hhvrc marked this pull request as ready for review August 13, 2026 11:12
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 184 files, which is 84 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ed905377-9712-4ea4-b4c2-744b4de0d047

📥 Commits

Reviewing files that changed from the base of the PR and between d5c6d7d and 909c374.

📒 Files selected for processing (184)
  • .github/workflows/ci-build.yml
  • .github/workflows/ci-tag.yml
  • .gitignore
  • DesktopRepositoryServer.Tests/DesktopRepositoryServer.Tests.csproj
  • DesktopRepositoryServer.Tests/UnitTest1.cs
  • DesktopRepositoryServer/AuthSchemas.cs
  • DesktopRepositoryServer/Config/ApiConfig.cs
  • DesktopRepositoryServer/Migrations/MigrationOpenShockContextModelSnapshot.cs
  • DesktopRepositoryServer/Models/CreateModuleRequest.cs
  • DesktopRepositoryServer/Problems/ModuleError.cs
  • DesktopRepositoryServer/RepoServerDb/Module.cs
  • DesktopRepositoryServer/RepoServerDb/RepoServerContext.cs
  • Directory.Packages.props
  • RepositoryServer.Tests/Integration/Docker/InMemoryDatabase.cs
  • RepositoryServer.Tests/Integration/TestAdminToken.cs
  • RepositoryServer.Tests/Integration/TestCiCdAuthHandler.cs
  • RepositoryServer.Tests/Integration/Tests/AdvisoriesAdminControllerTests.cs
  • RepositoryServer.Tests/Integration/Tests/BoardsAdminControllerTests.cs
  • RepositoryServer.Tests/Integration/Tests/ChipsAdminControllerTests.cs
  • RepositoryServer.Tests/Integration/Tests/DesktopCiCdControllerTests.cs
  • RepositoryServer.Tests/Integration/Tests/DiscordWebhooksControllerTests.cs
  • RepositoryServer.Tests/Integration/Tests/LatestControllerTests.cs
  • RepositoryServer.Tests/Integration/Tests/ManifestControllerTests.cs
  • RepositoryServer.Tests/Integration/Tests/PublicBoardsAndChipsControllerTests.cs
  • RepositoryServer.Tests/Integration/Tests/ReleasesControllerTests.cs
  • RepositoryServer.Tests/Integration/Tests/RepositoriesAdminControllerTests.cs
  • RepositoryServer.Tests/Integration/Tests/StagedReleaseCleanupServiceTests.cs
  • RepositoryServer.Tests/Integration/Tests/UsbDevicesAdminControllerTests.cs
  • RepositoryServer.Tests/Integration/Tests/UsbSerialFiltersAdminControllerTests.cs
  • RepositoryServer.Tests/Integration/Tests/VersionsControllerTests.cs
  • RepositoryServer.Tests/Integration/WebApplicationFactory.cs
  • RepositoryServer.Tests/RepositoryServer.Tests.csproj
  • RepositoryServer.Tests/Utils/ChangelogParserTests.cs
  • RepositoryServer.Tests/Utils/FirmwareArtifactFileNamesTests.cs
  • RepositoryServer.Tests/Utils/SourceUrlBuilderTests.cs
  • RepositoryServer/AuthSchemas.cs
  • RepositoryServer/AuthenticationHandlers/AdminTokenAuthentication.cs
  • RepositoryServer/AuthenticationHandlers/GitHubOidcAuthentication.cs
  • RepositoryServer/Config/ApiConfig.cs
  • RepositoryServer/Config/CiCdConfig.cs
  • RepositoryServer/Config/DbConfig.cs
  • RepositoryServer/Config/FirmwareConfig.cs
  • RepositoryServer/Config/MetricsConfig.cs
  • RepositoryServer/Config/RepoConfig.cs
  • RepositoryServer/Config/StorageConfig.cs
  • RepositoryServer/ConfigurationExtensions.cs
  • RepositoryServer/Controllers/OpenShockControllerBase.cs
  • RepositoryServer/Controllers/V1/AdminController.cs
  • RepositoryServer/Controllers/V1/CiCdController.cs
  • RepositoryServer/Controllers/V1/RepoController.cs
  • RepositoryServer/Controllers/V2/Admin/DiscordWebhooksController.cs
  • RepositoryServer/Controllers/V2/Firmware/Admin/AdvisoriesController.cs
  • RepositoryServer/Controllers/V2/Firmware/Admin/BoardsAdminController.cs
  • RepositoryServer/Controllers/V2/Firmware/Admin/ChipsAdminController.cs
  • RepositoryServer/Controllers/V2/Firmware/Admin/ReleasesAdminController.cs
  • RepositoryServer/Controllers/V2/Firmware/Admin/RepositoriesController.cs
  • RepositoryServer/Controllers/V2/Firmware/Admin/UsbDevicesController.cs
  • RepositoryServer/Controllers/V2/Firmware/Admin/UsbSerialFiltersController.cs
  • RepositoryServer/Controllers/V2/Firmware/Admin/VersionsAdminController.cs
  • RepositoryServer/Controllers/V2/Firmware/BoardsController.cs
  • RepositoryServer/Controllers/V2/Firmware/ChipsController.cs
  • RepositoryServer/Controllers/V2/Firmware/LatestController.cs
  • RepositoryServer/Controllers/V2/Firmware/ManifestController.cs
  • RepositoryServer/Controllers/V2/Firmware/ReleasesController.cs
  • RepositoryServer/Controllers/V2/Firmware/VersionsController.cs
  • RepositoryServer/Enums/AdvisorySeverity.cs
  • RepositoryServer/Enums/DiscordNotificationEvent.cs
  • RepositoryServer/Enums/FirmwareArtifactType.cs
  • RepositoryServer/Enums/FirmwareChipArchitecture.cs
  • RepositoryServer/Enums/ReleaseChannel.cs
  • RepositoryServer/Enums/ReleaseNoteSectionType.cs
  • RepositoryServer/Enums/ReleaseStatus.cs
  • RepositoryServer/Enums/RepositoryProvider.cs
  • RepositoryServer/Enums/RepositoryScope.cs
  • RepositoryServer/Errors/AuthResultError.cs
  • RepositoryServer/Errors/ExceptionError.cs
  • RepositoryServer/ExceptionHandler/ExceptionHandler.cs
  • RepositoryServer/ExceptionHandler/RequestInfo.cs
  • RepositoryServer/Migrations/20250315184152_Initial.Designer.cs
  • RepositoryServer/Migrations/20250315184152_Initial.cs
  • RepositoryServer/Migrations/20260813123629_UnifiedRepositoryServer.Designer.cs
  • RepositoryServer/Migrations/20260813123629_UnifiedRepositoryServer.cs
  • RepositoryServer/Migrations/MigrationOpenShockContextModelSnapshot.cs
  • RepositoryServer/Models/CreateModuleRequest.cs
  • RepositoryServer/Models/CreateModuleVersionRequest.cs
  • RepositoryServer/Models/Discord/DiscordWebhookDto.cs
  • RepositoryServer/Models/Discord/UpsertDiscordWebhookRequest.cs
  • RepositoryServer/Models/Firmware/CreateFirmwareBoardRequest.cs
  • RepositoryServer/Models/Firmware/CreateFirmwareChipRequest.cs
  • RepositoryServer/Models/Firmware/FirmwareAdvisoryAdminDto.cs
  • RepositoryServer/Models/Firmware/FirmwareAdvisoryDto.cs
  • RepositoryServer/Models/Firmware/FirmwareArtifactDto.cs
  • RepositoryServer/Models/Firmware/FirmwareBoardDetailDto.cs
  • RepositoryServer/Models/Firmware/FirmwareBoardDto.cs
  • RepositoryServer/Models/Firmware/FirmwareBoardReleaseResponseDto.cs
  • RepositoryServer/Models/Firmware/FirmwareChipDto.cs
  • RepositoryServer/Models/Firmware/FirmwareChipRefDto.cs
  • RepositoryServer/Models/Firmware/FirmwareManifestResponse.cs
  • RepositoryServer/Models/Firmware/FirmwareReleaseDto.cs
  • RepositoryServer/Models/Firmware/FirmwareReleaseNoteDto.cs
  • RepositoryServer/Models/Firmware/FirmwareSourceDto.cs
  • RepositoryServer/Models/Firmware/FirmwareUsbDeviceDto.cs
  • RepositoryServer/Models/Firmware/FirmwareUsbSerialFilterAdminDto.cs
  • RepositoryServer/Models/Firmware/FirmwareUsbSerialFilterDto.cs
  • RepositoryServer/Models/Firmware/FirmwareVersionSummary.cs
  • RepositoryServer/Models/Firmware/FixChangelogRequest.cs
  • RepositoryServer/Models/Firmware/InitReleaseRequest.cs
  • RepositoryServer/Models/Firmware/InitReleaseResponse.cs
  • RepositoryServer/Models/Firmware/RepositoryDto.cs
  • RepositoryServer/Models/Firmware/UpsertFirmwareAdvisoryRequest.cs
  • RepositoryServer/Models/Firmware/UpsertRepositoryRequest.cs
  • RepositoryServer/Models/Firmware/UpsertUsbDeviceRequest.cs
  • RepositoryServer/Models/Firmware/UpsertUsbSerialFilterRequest.cs
  • RepositoryServer/Models/Firmware/VersionListResponse.cs
  • RepositoryServer/Models/Module.cs
  • RepositoryServer/Models/Release/ChangeEntry.cs
  • RepositoryServer/Models/Release/NoticeEntry.cs
  • RepositoryServer/Models/Release/ReleaseData.cs
  • RepositoryServer/Models/Release/ReleaseNoteEntry.cs
  • RepositoryServer/Models/Release/Repository.cs
  • RepositoryServer/Models/Repository.cs
  • RepositoryServer/Models/Version.cs
  • RepositoryServer/Problems/DiscordError.cs
  • RepositoryServer/Problems/ExceptionProblem.cs
  • RepositoryServer/Problems/FirmwareError.cs
  • RepositoryServer/Problems/ModuleError.cs
  • RepositoryServer/Problems/OpenShockProblem.cs
  • RepositoryServer/Problems/ValidationProblem.cs
  • RepositoryServer/Problems/VersionError.cs
  • RepositoryServer/Program.cs
  • RepositoryServer/Properties/launchSettings.json
  • RepositoryServer/RepoServerDb/DesignTimeDbContextFactory.cs
  • RepositoryServer/RepoServerDb/DiscordWebhook.cs
  • RepositoryServer/RepoServerDb/FirmwareAdvisory.cs
  • RepositoryServer/RepoServerDb/FirmwareArtifact.cs
  • RepositoryServer/RepoServerDb/FirmwareBoard.cs
  • RepositoryServer/RepoServerDb/FirmwareBoardUsbDevice.cs
  • RepositoryServer/RepoServerDb/FirmwareChip.cs
  • RepositoryServer/RepoServerDb/FirmwareChipUsbDevice.cs
  • RepositoryServer/RepoServerDb/FirmwareRelease.cs
  • RepositoryServer/RepoServerDb/FirmwareReleaseNote.cs
  • RepositoryServer/RepoServerDb/FirmwareStagedArtifact.cs
  • RepositoryServer/RepoServerDb/FirmwareStagedReleaseNote.cs
  • RepositoryServer/RepoServerDb/FirmwareVersion.cs
  • RepositoryServer/RepoServerDb/Module.cs
  • RepositoryServer/RepoServerDb/RepoServerContext.cs
  • RepositoryServer/RepoServerDb/SourceRepository.cs
  • RepositoryServer/RepoServerDb/UsbDevice.cs
  • RepositoryServer/RepoServerDb/UsbSerialFilter.cs
  • RepositoryServer/RepoServerDb/Version.cs
  • RepositoryServer/RepositoryServer.csproj
  • RepositoryServer/Services/BunnyCdnStorageService.cs
  • RepositoryServer/Services/DiscordNotificationService.cs
  • RepositoryServer/Services/IDiscordNotificationService.cs
  • RepositoryServer/Services/IStorageService.cs
  • RepositoryServer/Services/LocalStorageService.cs
  • RepositoryServer/Services/S3StorageService.cs
  • RepositoryServer/Services/StagedReleaseCleanupService.cs
  • RepositoryServer/Utils/ByteArrayHexConverter.cs
  • RepositoryServer/Utils/CacheControlAttribute.cs
  • RepositoryServer/Utils/ChangelogParser.cs
  • RepositoryServer/Utils/ConnectionDetailsFetcher.cs
  • RepositoryServer/Utils/DiscordNotificationEventExtensions.cs
  • RepositoryServer/Utils/DiscordWebhookMasking.cs
  • RepositoryServer/Utils/EnumNaming.cs
  • RepositoryServer/Utils/FirmwareArtifactFileNames.cs
  • RepositoryServer/Utils/FirmwareBoardLookup.cs
  • RepositoryServer/Utils/FirmwareResponseMapper.cs
  • RepositoryServer/Utils/OpenShockEnricher.cs
  • RepositoryServer/Utils/ReleaseChannels.cs
  • RepositoryServer/Utils/RepositoryScopeExtensions.cs
  • RepositoryServer/Utils/SemVersionConverter.cs
  • RepositoryServer/Utils/SourceUrlBuilder.cs
  • RepositoryServer/Utils/TrustedProxiesFetcher.cs
  • RepositoryServer/Utils/UniqueViolation.cs
  • RepositoryServer/Utils/UriExtensions.cs
  • RepositoryServer/appsettings.Development.json
  • RepositoryServer/appsettings.json
  • RepositoryServers.slnx
  • docker/RepositoryServer.Dockerfile
  • docker/entrypoint.sh
  • firmware-api-spec.md
  • global.json
  • scripts/seed-catalog.sh

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

hhvrc added 9 commits August 13, 2026 13:17
…eness

Three defects, all downstream of making the board name do more work than a
label should.

Storage keys used the canonical board name, so renaming a board silently 404'd
every artifact ever published for it. Nothing records the name a blob was
written under, so it was not recoverable by query either — and UpdateBoard had
no guard, while DeleteBoard directly below it did. Storage keys now use the
board UUID, which never changes, so a rename is a pure metadata edit. The name
remains the public identifier everywhere it acts as a label: routes, response
boardId, the boards map key, ingestion and error messages. Consumers already
treat `url` as opaque, so nothing outside the server observes the difference.

Name uniqueness was case-sensitive while resolution was case-insensitive, so
ESP32-Core and esp32-core could coexist and both resolve to whichever sorted
first. The loser could never receive an upload, and a hub compiled with its
spelling would be served the other board's firmware — potentially built for a
different chip. A unique index on lower(name) makes that unrepresentable and
lets the lookups use an index instead of a sequential scan. Expression indexes
cannot be declared in the EF model, so the model-level index is relaxed to
non-unique and the real constraint is created in the migration; the migration
fails loudly if duplicates already exist, since auto-merging would orphan
artifacts.

Chips now identify by name on the public surface. Chip names are unique and
externally pinned — they must match esptool-js identifiers exactly, because the
flashtool passes them straight through — and no chip ever appears in a storage
path, so a surrogate key buys nothing there. /boards filters by ?chip= name.

Also fix the changelog parser, which dropped section prose whenever the section
also had bullets, and dropped indented list items entirely. The spec's own §5.3
worked example documents 8 notes and exercises both constructs; the golden test
passed only because its fixture omitted the two lines that would have exposed
this, and a second test asserted the dropped note stays dropped. The fixture is
now the spec's example verbatim, and the spec's expected output is corrected to
match its own concatenation rule.
…wnership

The ingestion path authenticated callers but never authorized them.

The OIDC handler inserted a repositories row for whatever repository_owner and
repository claims arrived, so the allowlist populated itself from whoever
showed up. A GitHub OIDC token proves only that some workflow somewhere on
GitHub requested one: the issuer is shared by every repository, and the
audience is a plain string any workflow can request by name and which ships in
plaintext in appsettings.json. Neither identifies the caller. Any GitHub
repository could therefore obtain a publishing principal. Registration is now
a lookup that fails closed, and onboarding is an explicit admin action via
PUT /v2/firmware/admin/repositories — an endpoint the spec has always
specified in §5.1 and §6.1, but which was never built. Removes the orphaned
CiCd.RepositoryOwner config key, which nothing read.

Authentication identifies a repository, not which release it may touch, and
every registered repository presents an equally valid principal. RepositoryId
was recorded at init and never consulted again, so upload, publish and abort
all resolved by releaseId alone: any authorized repository could inject
binaries into another's in-flight release, publish it under that repository's
identity and commit hash, or delete it. All three now re-check ownership and
return 403.

Desktop module publishing had the same hole and no ownership model at all —
modules gain an owning repository_id, and one with no owner assigned is closed
to every publisher rather than open to any. Republishing an existing module
version is also rejected: the upsert is keyed on (module, version), so it
previously replaced a published zip URL and hash in place, after which
integrity checks would pass against the replacement. Firmware likewise rejects
initialising a release for an already-published version, which otherwise let a
second release overwrite live artifacts at the same storage keys before any
publish call.

Test coverage for all of the above, which previously had none:
ReleasesController, the desktop CI/CD path, and the TTL cleanup service. The
cleanup pass is now internal rather than private so tests can drive it
deterministically — StartAsync returns at the first await inside ExecuteAsync,
so start-then-stop races the very tick it means to observe. A test auth handler
substitutes the JWT-validation half of the OIDC scheme; the allowlist lookup
itself is not covered by it, and is documented as verified by construction
instead. Replaces the test asserting the onboarding endpoint is absent.
H5: every "latest" and list query sorted only by release_date, which is
client-supplied and not unique — the spec's own example uses midnight. Ties
were broken arbitrarily by the database, so /manifest, /latest/{channel} and
/latest/{channel}/{board} could disagree with each other and flip between
requests, which a hub experiences as firmware flapping. Offset pagination had
the same problem and could duplicate and drop rows across pages. Ordering now
falls back to version, the primary key, giving a total order.

I3: channels now cascade, stable into beta into develop. CI advances all three
pointers when it ships a stable build, so a stable release is the newest thing
a beta subscriber should receive; strict per-channel equality pinned beta hubs
to the last explicit release candidate and offered them a downgrade.

H4: artifact upload deleted the board's prior staged rows and then hashed and
uploaded each file in turn, reporting mismatches at the end. One good file
alongside one bad one therefore left the good blob on the CDN with no row
referencing it — unreachable forever, since both abort and the TTL job
enumerate staged rows — while the board's previously valid staged state was
already gone. Everything is now verified before anything is deleted or
written, and the delete/upload/insert sequence runs in one transaction.

H2: the cleanup job read expired releases and then wrote Aborted
unconditionally, so a release hitting its TTL exactly as CI published it ended
up aborted in the database, live in the public API, and deleted from the CDN.
It now claims each release with a conditional update narrowed to the statuses
it decided on, and only deletes artifacts if the claim won. This also means
the tracked entity keeps its pre-abort status, so the log can finally say
which TTL fired.

H3: releaseDate is coerced to UTC on ingest. Npgsql rejects a non-zero offset
for `timestamp with time zone`, so any CI runner outside UTC got a bare 500.

H7: adds a partial unique index on version where status is staging or editing.
The controller's read-then-insert guard let two concurrent jobs for the same
tag both pass, after which they staged into the same CDN keys and raced each
other's uploads — a permanent hash mismatch for every OTA client, surfaced
nowhere. Both paths now return the same 409.

Board and chip name collisions return 409 rather than escaping as a 500, now
that lower(name) uniqueness is enforced by the database.

Adds scripts/seed-catalog.sh, which bootstraps the 13 PlatformIO boards, their
4 chips, and optionally the publish allowlist. A fresh database has no catalog
and InitRelease rejects unknown boards, so the first ingestion would otherwise
404.
Squashes the four migrations this branch adds into a single
UnifiedRepositoryServer migration, via `dotnet ef migrations remove` down to
the Initial migration that develop already has, then a fresh
`dotnet ef migrations add`. The expression and partial indexes are re-declared
in raw SQL because the EF model cannot express them, so the tooling could not
regenerate them: lower(name) uniqueness on boards and chips, and the partial
unique index on version for open releases.

Also removes whitespace-only churn against develop:

- ci-build.yml and ci-tag.yml carried trailing-whitespace edits on blank lines,
  which is where most of their diff came from. Rebuilt from develop's file with
  only the substantive renames applied, so their diffs are now entirely
  image/path changes.
- global.json had the same, rebuilt the same way.
- ApiConfig.cs, AdminController.cs and RepoController.cs had each gained a
  trailing newline that develop's copies do not have.

Note that `git diff -w` overstates this: it also collapses genuine structural
edits whose lines differ only by braces and indentation, which is why
Program.cs and appsettings.json appeared to carry churn and do not. Verified
with --ignore-space-at-eol and --ignore-blank-lines, which cannot be fooled
that way, and the rebuilt files are byte-identical to their previous content
under `git diff -w`.
…ublish

Uploads wrote straight to the published CDN key, so a version's artifacts were
readable at their final URL before the version existed, and a release that
failed midway left a half-populated version directory. It also meant abort and
the TTL job reconstructed live keys to delete, which is what made an abandoned
re-run able to remove artifacts a published version was serving.

Uploads now land under _staging/{releaseId}/, and PublishRelease copies each
object to its published key inside the publish path. A published key is
written exactly once; abort and TTL cleanup delete the release's own staging
prefix wholesale and cannot name a live key at all.

Storage is not transactional, so the promotion is ordered deliberately: copy
first, then commit. A database failure after copying leaves unreferenced
objects at published keys — invisible, since no version row points at them,
and overwritten by a later publish of the same version — and they are unwound
best-effort anyway. Committing first would instead publish a version whose
artifacts are not all present, which hubs would see as 404s mid-update.

Adds CopyFileAsync to IStorageService: server-side CopyObject on S3, File.Copy
locally, and a stream-through on Bunny, whose storage API has no copy.
Both files carry a BOM on develop and lost it during the project move, which
showed up as a spurious change to their first line. The repository is
inconsistent about BOMs generally — most carried-over files have one, most new
files do not — but that is pre-existing and not this PR's to settle; these two
are the only files whose BOM state actually changed.
Also corrects the artifact URL convention, which still described the board name
as the storage path segment.
…config

Firmware release ingestion and desktop module publishing share one
authentication scheme, and nothing distinguished them. Registering a repository
so it could publish desktop modules therefore also let it initialise and
publish firmware releases — the per-release ownership checks only stop it
hijacking someone else's in-flight release, not starting its own.

Registrations now carry scopes (publish_firmware, publish_modules). The OIDC
handler already loads the repository row, so it attaches them as claims, and
each ingestion controller sits behind an authorization policy requiring the
scope its grant was issued for. A repository registered with no scopes is
authenticated but cannot publish anything, which is the safe default for a new
row. Scopes are entirely server-side: a publishing workflow neither sends nor
sees them, so adopting this costs the repositories nothing.

Provider, owner and repo are now matched case-insensitively, enforced by a
unique index on (provider, lower(owner), lower(repo)). GitHub treats owner and
repo names case-insensitively and the casing in a token's claims follows
whatever the repository is currently named, so an exact match would let a
cosmetic rename silently de-authorize a repository — or let onboarding create a
second row that the handler's lookup would never reach.

The audience moves from Firmware:CiCd:Audience to a top-level CiCd:Audience. It
was nested under firmware config while also governing desktop publishing, and
appsettings.json already had a top-level CiCd block that the binding ignored.
It remains a single value: the audience is what makes an OIDC token
non-transferable between services, but rotating it is rare enough that
accepting a list was not worth the extra surface.

Also stops the DEBUG configuration dump printing secrets. GetDebugView()
emitted every config value and the process environment — admin token, database
password, storage credentials, Discord webhook URLs — to stdout on any Debug
build. It now prints resolved keys with credential-looking values masked, which
is what is actually useful for diagnosing binding.
Webhook targets were a flat list in configuration and every notification fanned
out to all of them, so maintainer alerts could not be routed away from a public
releases channel, and adding a channel meant a redeploy.

Targets now live in discord_webhooks, each subscribing to specific events, and
are managed through /v2/admin/discord-webhooks. The route sits outside
/firmware/admin because desktop module publishing notifies through the same
webhooks.

A webhook URL is a credential — possession of it is authorization to post to
that channel — so it is write-only. Responses carry a masked form that keeps
the Discord webhook id, which is what identifies the channel in Discord's own
UI, and drops the token. Delivery failures log the event rather than the URL.

Also fixes the delivery path, which was firing Task.Run with the triggering
request's CancellationToken and a typed HttpClient disposed with the request
scope. Notifications were therefore lost non-deterministically, including the
"needs editing" alert that is the only signal a release is stuck. The service is
now a singleton resolving its own HttpClient and DbContext, with its own
delivery timeout, since this work deliberately outlives the request.
hhvrc added 2 commits August 13, 2026 14:46
The shipped desktop app hardcodes https://repo.openshock.org/1 as a built-in
module repository (Desktop/Constants.cs, BuiltInModuleRepositories), and
develop's controllers route on "/{version:apiVersion}/". This branch had
changed them to "/v{version:apiVersion}/", so deploying it would have returned
404 to every installed desktop client for the module manifest and for module
version downloads.

firmware-api-spec.md documents the unprefixed form throughout ("/2/firmware/..."),
so the implementation disagreed with both the deployed clients and its own
contract. Routes now match: /1/ for the desktop endpoints and /2/firmware/...
for the firmware API.

Updates the two consumers written against the prefixed form in this branch —
the frontend client and the firmware OTA client, neither of which has shipped —
along with the CI ingestion action, the seeding script and the tests.
@hhvrc
hhvrc merged commit 6e99680 into develop Aug 13, 2026
12 checks passed
@hhvrc
hhvrc deleted the feature/unified-repository-server branch August 13, 2026 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant