feat: github_deb metadata-only package type

Add the Debian/apt analog of github_rpm: a metadata-only remote that scans
a GitHub repo's releases for .deb assets, derives per-asset control metadata
via a ranged prefix fetch (never downloading whole packages), synthesizes a
flat apt repository, and redirects .deb downloads to a backend releases_remote.

- Add PackageGitHubDeb to the package-type enum + validity map.
- Add the github_deb provider (internal/provider/deb/github.go): ServeRemote
  serves Packages/Packages.gz/Release, 404s the signed index variants
  (consumed via [trusted=yes]), and 302-redirects *.deb to the releases_remote;
  deriveAsset ranged-GETs the ar prefix, locates control.tar.*, and parses the
  control paragraph, doubling the range on truncation; sha256 comes from the
  asset digest when present, else a one-time full stream.
- Add the github_deb background Syncer (internal/provider/deb/syncer.go): its
  own worker pool, shared rate limiter, deduped queue, and DB-lease-gated scans.
- Add github_deb_sync_state table plus ListGitHubDebRemotes/Claim/Release DB
  helpers (separate from the rpm ones).
- Prime github_deb remotes on create and run the deb syncer alongside the rpm
  one; route prime-on-create by package type.
- Reuse the deb apt-index generators and control parser; skip empty hash lines
  in the Packages index so a SHA256-only metadata entry is valid.
This commit is contained in:
2026-08-11 22:21:05 +10:00
parent 60f008debc
commit b2a6be8eb5
11 changed files with 1889 additions and 14 deletions
+16 -2
View File
@@ -21,7 +21,7 @@ import (
"git.unkin.net/unkin/artifactapi/internal/gc"
"git.unkin.net/unkin/artifactapi/internal/githubauth"
_ "git.unkin.net/unkin/artifactapi/internal/provider/alpine"
_ "git.unkin.net/unkin/artifactapi/internal/provider/deb"
"git.unkin.net/unkin/artifactapi/internal/provider/deb"
_ "git.unkin.net/unkin/artifactapi/internal/provider/docker"
_ "git.unkin.net/unkin/artifactapi/internal/provider/generic"
_ "git.unkin.net/unkin/artifactapi/internal/provider/goproxy"
@@ -35,6 +35,7 @@ import (
"git.unkin.net/unkin/artifactapi/internal/storage"
"git.unkin.net/unkin/artifactapi/internal/tfsign"
"git.unkin.net/unkin/artifactapi/internal/virtual"
"git.unkin.net/unkin/artifactapi/pkg/models"
)
type Server struct {
@@ -50,6 +51,7 @@ type Server struct {
tfRegistry *tfregistry.Handler
gc *gc.Collector
syncer *rpm.Syncer
debSyncer *deb.Syncer
}
func New(cfg *config.Config, version string) (*Server, error) {
@@ -97,6 +99,12 @@ func New(cfg *config.Config, version string) (*Server, error) {
Workers: cfg.GitHubSyncWorkers,
PollInterval: time.Duration(cfg.GitHubSyncPollInterval) * time.Second,
})
debSyncer := deb.NewSyncer(db, deb.SyncConfig{
RatePerSec: cfg.GitHubSyncRatePerSec,
Burst: cfg.GitHubSyncBurst,
Workers: cfg.GitHubSyncWorkers,
PollInterval: time.Duration(cfg.GitHubSyncPollInterval) * time.Second,
})
// The terraform registry signs with a GPG key. A configured file wins (BYO
// key); otherwise artifactapi generates one on first start and persists it in
@@ -129,6 +137,7 @@ func New(cfg *config.Config, version string) (*Server, error) {
tfRegistry: tfRegistry,
gc: collector,
syncer: syncer,
debSyncer: debSyncer,
}
s.router = s.routes()
@@ -158,7 +167,10 @@ func (s *Server) routes() chi.Router {
r.Mount("/api/v1", proxyHandler.Routes())
r.Mount("/v2", proxyHandler.DockerV2Routes())
remotesHandler := v2.NewRemotesHandler(s.db, s.syncer)
remotesHandler := v2.NewRemotesHandler(s.db, map[models.PackageType]v2.Primer{
models.PackageGitHubRPM: s.syncer,
models.PackageGitHubDeb: s.debSyncer,
})
virtualsHandler := v2.NewVirtualsHandler(s.db)
healthHandler := v2.NewHealthHandler(s.db, s.cache, s.store)
statsHandler := v2.NewStatsHandler(s.db)
@@ -226,6 +238,7 @@ func (s *Server) newHTTPServer() *http.Server {
func (s *Server) Run(ctx context.Context) error {
go s.gc.Run(ctx)
go s.syncer.Run(ctx)
go s.debSyncer.Run(ctx)
httpServer := s.newHTTPServer()
@@ -247,6 +260,7 @@ func (s *Server) Run(ctx context.Context) error {
func (s *Server) RunOnListener(ctx context.Context, ln net.Listener) error {
go s.gc.Run(ctx)
go s.syncer.Run(ctx)
go s.debSyncer.Run(ctx)
httpServer := s.newHTTPServer()