Merge nucleic/mellow-dewy-falcon-rjhr into main
This commit is contained in:
@@ -88,8 +88,10 @@ public enum Doctor {
|
|||||||
/// than at the first poll.
|
/// than at the first poll.
|
||||||
/// 8. **Registration token resolvable** from file, inline value, or (if
|
/// 8. **Registration token resolvable** from file, inline value, or (if
|
||||||
/// enabled) the API.
|
/// enabled) the API.
|
||||||
/// 9. **Runner download URL is live**, via a `HEAD` expecting 200. Catches a
|
/// 9. **Runner download URL is live**, via a one-byte ranged `GET` — the
|
||||||
/// version bump that no longer has a darwin-arm64 asset.
|
/// 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
|
/// 10. **Local Network privacy note** (informational). On macOS 15+ the
|
||||||
/// first attempt to reach a guest over the NAT link can be blocked by
|
/// first attempt to reach a guest over the NAT link can be blocked by
|
||||||
/// the Local Network permission prompt, which a background agent cannot
|
/// the Local Network permission prompt, which a background agent cannot
|
||||||
@@ -455,16 +457,26 @@ public enum Doctor {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
// A ranged GET, not a HEAD. gitea.com answers an asset request with a
|
||||||
request.httpMethod = "HEAD"
|
// 303 to a presigned object-storage URL, and the signature covers the
|
||||||
request.timeoutInterval = 15
|
// *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 {
|
do {
|
||||||
let (_, response) = try await URLSession.shared.data(for: request)
|
let status = try await probeStatus(url: url, method: "GET", range: "bytes=0-0")
|
||||||
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
|
|
||||||
if status <= 399 {
|
if status <= 399 {
|
||||||
return DoctorCheck(name: name, result: .pass, detail: "\(url.absoluteString) → \(status)")
|
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(
|
return DoctorCheck(
|
||||||
name: name,
|
name: name,
|
||||||
result: .warn,
|
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.
|
/// Warns about token files readable by other users on this Mac.
|
||||||
public static func checkTokenFilePermissions(config: RunnerConfig) -> [DoctorCheck] {
|
public static func checkTokenFilePermissions(config: RunnerConfig) -> [DoctorCheck] {
|
||||||
let insecure = config.insecureTokenFilePaths
|
let insecure = config.insecureTokenFilePaths
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ gitea-macos-runner service status
|
|||||||
| Job stays queued forever | Label mismatch, or the daemon isn't running/reaching Gitea | Use bare label names in `runs-on`; match `runner.labels`; check daemon logs |
|
| Job stays queued forever | Label mismatch, or the daemon isn't running/reaching Gitea | Use bare label names in `runs-on`; match `runner.labels`; check daemon logs |
|
||||||
| `actions/checkout` fails instantly | Node.js missing from the guest image | `gitea-macos-runner image provision <name>` |
|
| `actions/checkout` fails instantly | Node.js missing from the guest image | `gitea-macos-runner image provision <name>` |
|
||||||
| Runner rows piling up in the Gitea UI | VMs killed uncleanly; registrations orphaned | Reconcile loop cleans them; force it by restarting the daemon; delete manually if needed |
|
| Runner rows piling up in the Gitea UI | VMs killed uncleanly; registrations orphaned | Reconcile loop cleans them; force it by restarting the daemon; delete manually if needed |
|
||||||
|
| `doctor` says `runner download url → 403` but the URL works in a browser | Old build: the check used `HEAD`, and the presigned redirect target is signed per method | Upgrade — the check now uses a ranged `GET`. If it persists, the asset really is missing |
|
||||||
| Disk filling up | Copy-on-write clones grow as jobs write | Raise `storage.minFreeDiskGB`; delete stale clones in `storeDir/vms` |
|
| Disk filling up | Copy-on-write clones grow as jobs write | Raise `storage.minFreeDiskGB`; delete stale clones in `storeDir/vms` |
|
||||||
| `image build` appears to hang during install | Normal — macOS install is slow | Wait. **Do not stop the VM mid-install**; if you did, delete the image and rebuild |
|
| `image build` appears to hang during install | Normal — macOS install is slow | Wait. **Do not stop the VM mid-install**; if you did, delete the image and rebuild |
|
||||||
|
|
||||||
@@ -261,6 +262,29 @@ registrations that have already been spent cannot receive jobs).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## `doctor` warns "runner download url → 403"
|
||||||
|
|
||||||
|
**Symptom.** `doctor` reports the runner download URL as a 403, but pasting the same URL into a
|
||||||
|
browser downloads the binary fine:
|
||||||
|
|
||||||
|
```
|
||||||
|
! runner download url https://gitea.com/gitea/runner/releases/download/v3.0.2/… → 403
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause.** gitea.com does not serve release assets itself. It answers with a `303 See Other`
|
||||||
|
pointing at a presigned object-storage URL, and that signature covers the HTTP method of the
|
||||||
|
request that minted it. A `HEAD` probe therefore gets a HEAD-signed URL — and then the HTTP client,
|
||||||
|
following the 303, rewrites the method to `GET` (RFC 7231 §6.4.4) and replays the signed URL with
|
||||||
|
the one verb it was not signed for. The store answers `403 SignatureDoesNotMatch`. Nothing is
|
||||||
|
actually wrong with the asset.
|
||||||
|
|
||||||
|
**Fix.** Upgrade — the check now probes with a one-byte ranged `GET` (`Range: bytes=0-0`), the same
|
||||||
|
verb `image provision` uses for the real download, and falls back to a `HEAD` only if the range is
|
||||||
|
rejected. If you still see a 403 after upgrading, the asset really is gone: check `runner.version`
|
||||||
|
and `runner.runnerDownloadURL` for a `darwin-arm64` build.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Disk filling up
|
## Disk filling up
|
||||||
|
|
||||||
**Symptom.** Free space falls steadily; the daemon starts refusing to launch VMs, citing
|
**Symptom.** Free space falls steadily; the daemon starts refusing to launch VMs, citing
|
||||||
|
|||||||
Reference in New Issue
Block a user