fix: harden release checks and provider errors
CI / build-and-test (push) Canceled after 0s
CI / provider-contracts (push) Canceled after 0s
CI / compose-contracts (push) Canceled after 0s

This commit is contained in:
2026-07-21 23:42:45 -04:00
parent 2d3a8f8ae7
commit 3ea5ad1bae
8 changed files with 51 additions and 16 deletions
+4 -2
View File
@@ -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),
+2 -2
View File
@@ -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;
}
}
+4 -4
View File
@@ -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;
}
+1 -1
View File
@@ -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')"
+24
View File
@@ -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()