Nucleic: Gitea Runner macOS VM Support

This commit is contained in:
2026-08-07 01:02:32 -07:00
parent 11019d498b
commit 9edaa3e409
2 changed files with 57 additions and 8 deletions
+33 -8
View File
@@ -88,8 +88,10 @@ public enum Doctor {
/// than at the first poll.
/// 8. **Registration token resolvable** from file, inline value, or (if
/// enabled) the API.
/// 9. **Runner download URL is live**, via a `HEAD` expecting 200. Catches a
/// version bump that no longer has a darwin-arm64 asset.
/// 9. **Runner download URL is live**, via a one-byte ranged `GET` — the
/// same verb the real download uses, because the presigned redirect
/// target is signed per method. Catches a version bump that no longer
/// has a darwin-arm64 asset.
/// 10. **Local Network privacy note** (informational). On macOS 15+ the
/// first attempt to reach a guest over the NAT link can be blocked by
/// the Local Network permission prompt, which a background agent cannot
@@ -455,16 +457,26 @@ public enum Doctor {
)
}
var request = URLRequest(url: url)
request.httpMethod = "HEAD"
request.timeoutInterval = 15
// A ranged GET, not a HEAD. gitea.com answers an asset request with a
// 303 to a presigned object-storage URL, and the signature covers the
// *method of the request that minted it*: ask with HEAD and you get a
// HEAD-signed URL. URLSession then follows the 303 and — per RFC 7231
// §6.4.4 — rewrites the method to GET, so the signed URL is replayed
// with the one verb it was not signed for and the store answers 403
// SignatureDoesNotMatch. Probing with the same verb the real download
// uses is the only way to make the answer mean anything. `bytes=0-0`
// keeps it to one byte instead of the whole 20-plus MB asset.
do {
let (_, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
let status = try await probeStatus(url: url, method: "GET", range: "bytes=0-0")
if status <= 399 {
return DoctorCheck(name: name, result: .pass, detail: "\(url.absoluteString) → \(status)")
}
// A host that rejects ranges outright still deserves a second look
// before we call the asset missing.
let fallback = try await probeStatus(url: url, method: "HEAD", range: nil)
if fallback <= 399 {
return DoctorCheck(name: name, result: .pass, detail: "\(url.absoluteString) → \(fallback)")
}
return DoctorCheck(
name: name,
result: .warn,
@@ -483,6 +495,19 @@ public enum Doctor {
}
}
/// Issues one probe request and reports its status code, or 0 if the
/// response was not HTTP.
private static func probeStatus(url: URL, method: String, range: String?) async throws -> Int {
var request = URLRequest(url: url)
request.httpMethod = method
request.timeoutInterval = 15
if let range {
request.setValue(range, forHTTPHeaderField: "Range")
}
let (_, response) = try await URLSession.shared.data(for: request)
return (response as? HTTPURLResponse)?.statusCode ?? 0
}
/// Warns about token files readable by other users on this Mac.
public static func checkTokenFilePermissions(config: RunnerConfig) -> [DoctorCheck] {
let insecure = config.insecureTokenFilePaths