Hourly scheduled builds via a ??:MM slot
`atTimes: ["??:05"]` runs a build five past every hour. The pattern expands to its 24 concrete slots before the due-slot match, so each hour is its own slot in the trigger state and fires once — the existing per-slot semantics carry over unchanged, including that only the latest due slot of a day triggers and that a slot whose pool is still building is retried until it starts. Only the hour may be a wildcard; anything else is skipped with a warning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
08a8a0bbb4
commit
939d8eeb9c
@@ -118,7 +118,7 @@ builds:
|
||||
# Example of a named build definition; all keys except its name are optional:
|
||||
# pitest:
|
||||
# onPush: false # trigger: build every new commit (default: false)
|
||||
# atTimes: ["01:00"] # trigger: daily UTC times HH:MM (default: none)
|
||||
# atTimes: ["01:00"] # trigger: daily UTC times HH:MM, "??:05" = hourly at :05
|
||||
# branches: ["master", "release/*"] # selector: names or glob patterns (default: all)
|
||||
# activeWithin: 24h # selector: only branches with commits in the last 24h
|
||||
# buildCommand: ./gradlew piTestFull # overrides; unset keys fall back to the
|
||||
@@ -265,6 +265,8 @@ Every key of the `builds` section names a build definition (a job) over the bran
|
||||
A build definition has triggers, a branch selector, and build-setting overrides.
|
||||
|
||||
Triggers: `onPush: true` builds every new commit of the selected branches; `atTimes: ["HH:MM", …]` rebuilds their heads once per day and slot (UTC).
|
||||
A slot may also be written as `??:MM` — that minute of every hour, expanded to its 24 slots, so the build runs hourly.
|
||||
Only the latest due slot of a day triggers, so slots missed while the server was down are skipped instead of piling up, and a slot whose pool is still building is retried on the next poll cycle until it succeeds.
|
||||
A definition may have both; one with neither never triggers automatically.
|
||||
|
||||
Selector: `branches` lists branch names or glob patterns (`*` matches any characters, also across `/`); empty selects all origin branches.
|
||||
|
||||
@@ -162,7 +162,7 @@ class InitCommand(
|
||||
# Example definition — triggers (onPush/atTimes), branch selector
|
||||
# (branches/activeWithin), and overrides of the branch settings:
|
||||
# pitest:
|
||||
# atTimes: ["01:00"] # daily UTC times HH:MM
|
||||
# atTimes: ["01:00"] # daily UTC times HH:MM ("??:05" = every hour at :05)
|
||||
# branches: ["master"] # names or glob patterns; default: all branches
|
||||
# activeWithin: 24h # only branches with recent commits
|
||||
# buildCommand: ./gradlew piTestFull
|
||||
|
||||
@@ -16,7 +16,11 @@ import java.time.Instant
|
||||
data class BuildDefinition(
|
||||
/** Build every new commit of the selected branches. */
|
||||
val onPush: Boolean = false,
|
||||
/** Daily UTC times `HH:MM`; each slot rebuilds the selected branches' heads once per day. */
|
||||
/**
|
||||
* Daily UTC times `HH:MM`; each slot rebuilds the selected branches' heads once per day.
|
||||
* `??:MM` is the hourly form — it stands for that minute of every hour, so each of its
|
||||
* 24 slots triggers separately.
|
||||
*/
|
||||
val atTimes: List<String> = emptyList(),
|
||||
/**
|
||||
* Branch names or glob patterns (`*` matches any characters, also across `/`);
|
||||
|
||||
@@ -29,22 +29,41 @@ data class AutoBuildTrigger(
|
||||
object AutoBuildSlots {
|
||||
private val log = LoggerFactory.getLogger(AutoBuildSlots::class.java)
|
||||
|
||||
/** The latest valid slot at or before [now], or null when no slot is due yet today. */
|
||||
/**
|
||||
* The latest valid slot at or before [now], or null when no slot is due yet today.
|
||||
* The returned slot is always a concrete `HH:MM` — an hourly pattern is expanded
|
||||
* first, so each of its hours triggers separately.
|
||||
*/
|
||||
fun latestDueSlot(
|
||||
times: List<String>,
|
||||
now: LocalTime,
|
||||
): String? =
|
||||
times
|
||||
.flatMap { expand(it) }
|
||||
.mapNotNull { slot ->
|
||||
try {
|
||||
LocalTime.parse(slot.trim()) to slot
|
||||
LocalTime.parse(slot) to slot
|
||||
} catch (_: DateTimeParseException) {
|
||||
log.warn("skipping invalid scheduled-build time slot '{}': expected HH:MM", slot)
|
||||
log.warn("skipping invalid scheduled-build time slot '{}': expected HH:MM or ??:MM", slot)
|
||||
null
|
||||
}
|
||||
}.filter { (parsed, _) -> !parsed.isAfter(now) }
|
||||
.maxByOrNull { (parsed, _) -> parsed }
|
||||
?.second
|
||||
|
||||
/** `??:MM` means every hour at that minute and expands to its 24 slots; `HH:MM` is itself. */
|
||||
private fun expand(time: String): List<String> {
|
||||
val slot = time.trim()
|
||||
if (!slot.startsWith("??:")) {
|
||||
return listOf(slot)
|
||||
}
|
||||
val minute = slot.substringAfter(':').toIntOrNull()
|
||||
if (minute == null || minute !in 0..59) {
|
||||
log.warn("skipping invalid scheduled-build time slot '{}': expected ??:MM with MM from 00 to 59", slot)
|
||||
return emptyList()
|
||||
}
|
||||
return (0..23).map { hour -> "%02d:%02d".format(hour, minute) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,30 @@ class AutoBuildStateTest : FunSpec() {
|
||||
AutoBuildSlots.latestDueSlot(listOf("25:99"), LocalTime.parse("12:00")).shouldBeNull()
|
||||
}
|
||||
|
||||
test("an hourly ??:MM slot is due every hour and resolves to that hour's concrete slot") {
|
||||
val times = listOf("??:05")
|
||||
|
||||
AutoBuildSlots.latestDueSlot(times, LocalTime.parse("00:04")).shouldBeNull()
|
||||
AutoBuildSlots.latestDueSlot(times, LocalTime.parse("00:05")) shouldBe "00:05"
|
||||
AutoBuildSlots.latestDueSlot(times, LocalTime.parse("07:30")) shouldBe "07:05"
|
||||
// the next hour is a different slot, so it triggers again
|
||||
AutoBuildSlots.latestDueSlot(times, LocalTime.parse("08:05")) shouldBe "08:05"
|
||||
AutoBuildSlots.latestDueSlot(times, LocalTime.parse("23:59")) shouldBe "23:05"
|
||||
}
|
||||
|
||||
test("hourly and fixed slots combine, the latest due one wins") {
|
||||
val times = listOf("??:05", "12:30")
|
||||
|
||||
AutoBuildSlots.latestDueSlot(times, LocalTime.parse("12:20")) shouldBe "12:05"
|
||||
AutoBuildSlots.latestDueSlot(times, LocalTime.parse("12:45")) shouldBe "12:30"
|
||||
AutoBuildSlots.latestDueSlot(times, LocalTime.parse("13:10")) shouldBe "13:05"
|
||||
}
|
||||
|
||||
test("latestDueSlot skips an hourly slot with an impossible minute") {
|
||||
AutoBuildSlots.latestDueSlot(listOf("??:70"), LocalTime.parse("12:00")).shouldBeNull()
|
||||
AutoBuildSlots.latestDueSlot(listOf("??:xx", "02:00"), LocalTime.parse("12:00")) shouldBe "02:00"
|
||||
}
|
||||
|
||||
test("latestDueSlot of an empty slot list is null") {
|
||||
AutoBuildSlots.latestDueSlot(emptyList(), LocalTime.parse("12:00")).shouldBeNull()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user