mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-08-19 12:32:34 -04:00
fix: harden release checks and provider errors
This commit is contained in:
@@ -15,6 +15,8 @@ env:
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
app-version: ${{ steps.app-version.outputs.version }}
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18.4-alpine3.23@sha256:996d0920e4ff9df1fc19dacb904492f3c1ec0ec1cc338f0ad7123be7731c5f5e
|
||||
@@ -142,7 +144,7 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
tags: allstarr:release-smoke
|
||||
build-args: ALLSTARR_VERSION=${{ steps.app-version.outputs.version }}
|
||||
build-args: ALLSTARR_VERSION=${{ needs.build-and-test.outputs.app-version }}
|
||||
cache-from: type=gha
|
||||
|
||||
- name: Smoke test built image
|
||||
@@ -220,7 +222,7 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: ALLSTARR_VERSION=${{ steps.app-version.outputs.version }}
|
||||
build-args: ALLSTARR_VERSION=${{ needs.build-and-test.outputs.app-version }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
|
||||
@@ -101,6 +101,10 @@ public sealed class AppleMusicKitPlaylistCapabilityAdapterTests
|
||||
handler.ArtworkBytes = new byte[17];
|
||||
var oversized = await adapter.ResolveArtworkAsync(Context(), new(reference, 16));
|
||||
Assert.Equal(ProviderErrorKind.PermanentFailure, oversized.Error!.Kind);
|
||||
|
||||
handler.ArtworkFailure = HttpStatusCode.Forbidden;
|
||||
var forbidden = await adapter.ResolveArtworkAsync(Context(), new(reference, 16));
|
||||
Assert.Equal(ProviderErrorKind.PermanentFailure, forbidden.Error!.Kind);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -157,6 +161,7 @@ public sealed class AppleMusicKitPlaylistCapabilityAdapterTests
|
||||
private sealed class AppleHandler : HttpMessageHandler
|
||||
{
|
||||
public HttpStatusCode? Failure { get; set; }
|
||||
public HttpStatusCode? ArtworkFailure { get; set; }
|
||||
public List<string> Paths { get; } = [];
|
||||
public List<string> Authorization { get; } = [];
|
||||
public List<string> UserTokens { get; } = [];
|
||||
@@ -165,7 +170,7 @@ public sealed class AppleMusicKitPlaylistCapabilityAdapterTests
|
||||
{
|
||||
Paths.Add(request.RequestUri!.PathAndQuery);
|
||||
if (request.RequestUri.Host == "is1-ssl.mzstatic.com")
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||
return Task.FromResult(new HttpResponseMessage(ArtworkFailure ?? HttpStatusCode.OK)
|
||||
{
|
||||
Content = new ByteArrayContent(ArtworkBytes)
|
||||
{
|
||||
|
||||
@@ -317,8 +317,11 @@ public sealed class PlaylistLinksController(
|
||||
.ToListAsync(cancellationToken);
|
||||
var snapshotsByLink = snapshots.ToDictionary(item => item.PlaylistLinkId);
|
||||
var runsByLink = runs.ToDictionary(item => item.PlaylistLinkId);
|
||||
return Ok(new { playlistLinks = links.Select(link => ToListDto(link,
|
||||
snapshotsByLink.GetValueOrDefault(link.Id), runsByLink.GetValueOrDefault(link.Id))) });
|
||||
return Ok(new
|
||||
{
|
||||
playlistLinks = links.Select(link => ToListDto(link,
|
||||
snapshotsByLink.GetValueOrDefault(link.Id), runsByLink.GetValueOrDefault(link.Id)))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ public sealed class AppleMusicKitPlaylistCapabilityAdapter : IProviderPlaylistCa
|
||||
using var response = await _http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
if (response.RequestMessage?.RequestUri is { } finalUri && !IsAllowedArtworkHost(finalUri.Host))
|
||||
return ProviderOutcome<ProviderPlaylistArtwork>.Failure(new(ProviderErrorKind.PermanentFailure));
|
||||
if (!response.IsSuccessStatusCode) return ProviderOutcome<ProviderPlaylistArtwork>.Failure(Error(response));
|
||||
if (!response.IsSuccessStatusCode) return ProviderOutcome<ProviderPlaylistArtwork>.Failure(Error(response, accountBound: false));
|
||||
var contentType = response.Content.Headers.ContentType?.MediaType?.ToLowerInvariant();
|
||||
if (contentType is not ("image/jpeg" or "image/png" or "image/webp") ||
|
||||
response.Content.Headers.ContentLength > maximumBytes)
|
||||
@@ -312,10 +312,11 @@ public sealed class AppleMusicKitPlaylistCapabilityAdapter : IProviderPlaylistCa
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ProviderError Error(HttpResponseMessage response) => response.StatusCode switch
|
||||
private static ProviderError Error(HttpResponseMessage response, bool accountBound = true) => response.StatusCode switch
|
||||
{
|
||||
HttpStatusCode.Unauthorized => new(ProviderErrorKind.AccountNeedsReauthentication),
|
||||
HttpStatusCode.Forbidden => new(ProviderErrorKind.AccountNeedsReauthentication),
|
||||
HttpStatusCode.Unauthorized when accountBound => new(ProviderErrorKind.AccountNeedsReauthentication),
|
||||
HttpStatusCode.Forbidden when accountBound => new(ProviderErrorKind.AccountNeedsReauthentication),
|
||||
HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden => new(ProviderErrorKind.PermanentFailure),
|
||||
HttpStatusCode.NotFound => new(ProviderErrorKind.NotFound),
|
||||
HttpStatusCode.TooManyRequests => new(ProviderErrorKind.RateLimited, response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(30)),
|
||||
>= HttpStatusCode.InternalServerError => new(ProviderErrorKind.TransientFailure),
|
||||
|
||||
@@ -484,11 +484,11 @@
|
||||
|
||||
.settings-view > details.content-disclosure.panel > summary {
|
||||
min-height: 42px;
|
||||
padding: 6px 12px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.settings-view > details.content-disclosure.panel > .disclosure-body {
|
||||
padding: 10px 12px 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -789,7 +789,7 @@
|
||||
|
||||
/* Settings disclosures are compact rows; expanded content keeps its own inset. */
|
||||
.settings-view {
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-view > details.content-disclosure.panel {
|
||||
@@ -806,8 +806,8 @@
|
||||
box-sizing: border-box;
|
||||
min-height: 44px;
|
||||
height: auto;
|
||||
gap: 10px;
|
||||
padding: 6px 14px;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.settings-view > details.content-disclosure.panel > summary strong {
|
||||
@@ -832,7 +832,7 @@
|
||||
}
|
||||
|
||||
.settings-view > details.content-disclosure.panel > .disclosure-body {
|
||||
padding: 12px 14px 14px;
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ test -n "${version}" || {
|
||||
|
||||
commit="unavailable"
|
||||
tag="unavailable"
|
||||
dirty="unknown"
|
||||
dirty="null"
|
||||
if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
commit="$(git rev-parse HEAD)"
|
||||
tag="$(git describe --tags --exact-match 2>/dev/null || printf 'untagged')"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
@@ -41,6 +43,28 @@ class ReleaseManifestTests(unittest.TestCase):
|
||||
for digest in manifest["digests"].values():
|
||||
self.assertRegex(digest, r"^[0-9a-f]{64}$")
|
||||
|
||||
def test_manifest_remains_valid_json_when_git_metadata_is_unavailable(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
fake_git = pathlib.Path(temporary_directory) / "git"
|
||||
fake_git.write_text("#!/bin/sh\nexit 127\n", encoding="utf-8")
|
||||
fake_git.chmod(0o755)
|
||||
environment = os.environ.copy()
|
||||
environment["PATH"] = f"{temporary_directory}:{environment['PATH']}"
|
||||
|
||||
completed = subprocess.run(
|
||||
["bash", "tools/create-release-manifest.sh"],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=environment,
|
||||
)
|
||||
|
||||
manifest = json.loads(completed.stdout)
|
||||
self.assertEqual("unavailable", manifest["git"]["commit"])
|
||||
self.assertEqual("unavailable", manifest["git"]["tag"])
|
||||
self.assertIsNone(manifest["git"]["trackedFilesDirty"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user