85 lines
4.0 KiB
Swift
85 lines
4.0 KiB
Swift
import Foundation
|
|
|
|
/// The set of labels this host's runners advertise, used to decide whether a
|
|
/// queued Gitea job is ours to pick up.
|
|
///
|
|
/// ## Bare names only
|
|
///
|
|
/// Gitea's label syntax at *registration* time is `name:schema` (for example
|
|
/// `macos-arm64:host`), where the schema defaults to `host` when omitted. The
|
|
/// schema is a runner-side execution hint — it tells `gitea-runner` to run the
|
|
/// job directly on the machine instead of inside a container. **The server only
|
|
/// ever stores and reports the bare name.** A workflow's `runs-on:` value, and
|
|
/// therefore the `labels` array on a queued job, likewise contains bare names.
|
|
///
|
|
/// So: pass `macos-arm64:host` to `gitea-runner register --labels`, but match
|
|
/// against `macos-arm64` here. Matching is case-sensitive, because Gitea's own
|
|
/// comparison is.
|
|
///
|
|
/// - Warning: If the guest's `.runner`/`config.yaml` sets `runner.labels`, it
|
|
/// silently overrides whatever `--labels` was passed at registration. The
|
|
/// guest must therefore never ship a config file containing labels.
|
|
public struct LabelSet: Sendable, Equatable, Hashable {
|
|
/// The bare label names this host serves, e.g. `["macos-arm64", "macos"]`.
|
|
public let names: Set<String>
|
|
|
|
/// Creates a label set from bare names.
|
|
///
|
|
/// Any `:schema` suffix present in `names` is stripped, so it is safe to
|
|
/// hand this the same array that is written into the config file.
|
|
///
|
|
/// - Parameter names: Label names, with or without a `:schema` suffix.
|
|
public init(_ names: [String]) {
|
|
self.names = Set(
|
|
names
|
|
.map(LabelSet.bareName)
|
|
.filter { !$0.isEmpty }
|
|
)
|
|
}
|
|
|
|
/// Whether a queued job's `labels` array can be satisfied by this host.
|
|
///
|
|
/// Returns `true` if and only if `jobLabels` is non-empty *and* every entry
|
|
/// is a member of ``names``. An empty job label array is treated as "no
|
|
/// declared requirement" and is deliberately **not** matched — a job that
|
|
/// asks for nothing must not be scheduled onto a scarce macOS VM.
|
|
///
|
|
/// The job side is put through ``bareName(_:)`` too. The server normally
|
|
/// stores bare names, so this changes nothing in the common case — but a
|
|
/// workflow that writes `runs-on: [macos-arm64:host]` would otherwise never
|
|
/// match anything and its job would be skipped with no log line at all.
|
|
///
|
|
/// - Parameter jobLabels: The `labels` array from a `WorkflowJob`.
|
|
/// - Returns: `true` when this host should boot a VM for the job.
|
|
public func matches(jobLabels: [String]) -> Bool {
|
|
guard !jobLabels.isEmpty else { return false }
|
|
let wanted = Set(jobLabels.map(LabelSet.bareName).filter { !$0.isEmpty })
|
|
guard !wanted.isEmpty else { return false }
|
|
return wanted.isSubset(of: names)
|
|
}
|
|
|
|
/// The value to pass to `gitea-runner register --labels`, i.e. each bare
|
|
/// name suffixed with the given schema and joined by commas.
|
|
///
|
|
/// - Parameter schema: The execution schema; `host` for a bare-metal guest.
|
|
/// - Returns: For example `"macos-arm64:host,macos:host"`.
|
|
public func registrationArgument(schema: String = "host") -> String {
|
|
// Sorted so the argument is stable across process runs — a `Set` has no
|
|
// inherent order, and an unstable registration argument would make the
|
|
// guest command line (and its logs) needlessly non-reproducible.
|
|
names.sorted()
|
|
.map { schema.isEmpty ? $0 : "\($0):\(schema)" }
|
|
.joined(separator: ",")
|
|
}
|
|
|
|
/// Strips an optional `:schema` suffix from a single label token.
|
|
///
|
|
/// - Parameter label: A label such as `macos-arm64:host` or `macos-arm64`.
|
|
/// - Returns: The bare name.
|
|
public static func bareName(_ label: String) -> String {
|
|
let trimmed = label.trimmingCharacters(in: .whitespaces)
|
|
guard let colon = trimmed.firstIndex(of: ":") else { return trimmed }
|
|
return String(trimmed[trimmed.startIndex..<colon])
|
|
}
|
|
}
|