commit 87a14b7840ca39ee319db9a388b0dfe4e0b87b64
parent 0fc217c3273ef0e004a8614b86150d92de6de1a4
Author: triesap <tyson@radroots.org>
Date: Tue, 11 Aug 2026 03:55:38 +0000
build: separate build contract tests from verification tasks
- keep production verification focused on declared repository inputs
- move parser and policy mutation fixtures into ordinary tests
- declare the root Makefile as a tracked Gradle task input
- retain exact product, provenance, and lane validation behavior
Diffstat:
5 files changed, 125 insertions(+), 130 deletions(-)
diff --git a/build-logic/contracts/src/test/kotlin/org/harvestcircle/buildlogic/contracts/BuildContractsTest.kt b/build-logic/contracts/src/test/kotlin/org/harvestcircle/buildlogic/contracts/BuildContractsTest.kt
@@ -17,11 +17,49 @@ class BuildContractsTest {
coordinates.digest,
)
assertEquals(coordinates.digest, ProductCoordinates.parse(productCoordinates.replace("\n", "\r\n")).digest)
+ assertEquals(coordinates.digest, ProductCoordinates.parse(productCoordinates.trimEnd()).digest)
assertEquals(coordinates.digest, ProductCoordinates.parse("# comment\n$productCoordinates").digest)
+ assertEquals(
+ coordinates.digest,
+ ProductCoordinates.parse(
+ productCoordinates.lineSequence().joinToString("\n") { line ->
+ if (line.isBlank()) line else line.replaceFirst("=", " = ")
+ },
+ ).digest,
+ )
assertFails { ProductCoordinates.parse("\uFEFF$productCoordinates") }
assertFails { ProductCoordinates.parse(productCoordinates + "schema=harvestcircle.product.v1\n") }
assertFails { ProductCoordinates.parse(productCoordinates + "unknown=value\n") }
+ assertFails { ProductCoordinates.parse(productCoordinates.substringAfter('\n')) }
+ assertFails { ProductCoordinates.parse(productCoordinates.replace("product.name=HarvestCircle", "product.name")) }
+ assertFails { ProductCoordinates.parse(productCoordinates.replaceCoordinate("product.slug", "INVALID")) }
assertFails { ProductCoordinates.parse(productCoordinates.replace("harvestcircle.sqlite3", "../other.sqlite3")) }
+
+ val validMutations =
+ linkedMapOf(
+ "product.name" to "Harvest Circle Test",
+ "product.slug" to "harvestcircle_test",
+ "kotlin.root_namespace" to "org.example",
+ "desktop.application_id" to "org.example.desktop",
+ "desktop.bundle_id" to "org.example.bundle",
+ "desktop.main_class" to "org.example.MainKt",
+ "ffi.kotlin_package" to "org.example.ffi",
+ "ffi.cdylib_name" to "example_ffi",
+ "database.qualifier" to "com",
+ "database.organization" to "example",
+ "database.application" to "test",
+ "database.filename" to "example.sqlite3",
+ "keyring.service" to "org.example.desktop.nostr",
+ "environment.prefix" to "EXAMPLE_",
+ "vendor.name" to "Example Cooperative",
+ "copyright.notice" to "Copyright Example contributors",
+ )
+ assertEquals(ProductCoordinates.requiredKeys.size - 1, validMutations.size)
+ validMutations.forEach { (key, replacement) ->
+ val mutated = ProductCoordinates.parse(productCoordinates.replaceCoordinate(key, replacement))
+ assertEquals(replacement, mutated[key])
+ assertTrue(mutated.digest != coordinates.digest)
+ }
}
@Test
@@ -31,7 +69,14 @@ class BuildContractsTest {
assertEquals("harvestcircle-desktop-ffi-v4", baseline["contract.id"])
assertFails { FfiCompatibilityBaseline.parse("\uFEFF$ffiBaseline") }
assertFails { FfiCompatibilityBaseline.parse(ffiBaseline + "unknown=value\n") }
+ assertFails { FfiCompatibilityBaseline.parse(ffiBaseline + "contract.id=harvestcircle-desktop-ffi-v4\n") }
+ assertFails { FfiCompatibilityBaseline.parse(ffiBaseline.substringAfter('\n')) }
+ assertFails { FfiCompatibilityBaseline.parse(ffiBaseline.replace("contract.id=", "contract.id")) }
assertFails { FfiCompatibilityBaseline.parse(ffiBaseline.replace("contract.hash=${"a".repeat(64)}", "contract.hash=bad")) }
+ assertFails { FfiCompatibilityBaseline.parse(ffiBaseline.replace("product.version=0.1.0-alpha", "product.version=invalid")) }
+ assertFails { FfiCompatibilityBaseline.parse(ffiBaseline.replace("package.version=1.0.0", "package.version=invalid")) }
+ assertFails { FfiCompatibilityBaseline.parse(ffiBaseline.replace("schema=harvestcircle.ffi.v4", "schema=harvestcircle.ffi.v3")) }
+ assertFails { FfiCompatibilityBaseline.parse(ffiBaseline.replace("contract.major=4", "contract.major=3")) }
assertFails { FfiCompatibilityBaseline.parse(ffiBaseline.replace("contract.minor=1", "contract.minor=2")) }
}
@@ -45,11 +90,23 @@ class BuildContractsTest {
)
assertEquals(canonical.digest, SourceProvenance.parse(sourceProvenance.replace("\n", "\r\n")).digest)
+ assertEquals(canonical.digest, SourceProvenance.parse("# comment\n$sourceProvenance").digest)
assertEquals(canonical.digest, SourceProvenance.parse(reordered).digest)
assertEquals("a".repeat(40), canonical.foundationBaseline)
assertFails { SourceProvenance.parse("\uFEFF$sourceProvenance") }
assertFails { SourceProvenance.parse("unknown = \"value\"\n$sourceProvenance") }
+ assertFails { SourceProvenance.parse(sourceProvenance.replace("schema = ", "schema ")) }
+ assertFails { SourceProvenance.parse(sourceProvenance.substringAfter('\n')) }
+ assertFails { SourceProvenance.parse(sourceProvenance.replace("source_product = \"HarvestCircle\"", "source_product = \"HarvestCircle\"\nsource_product = \"HarvestCircle\"")) }
assertFails { SourceProvenance.parse(sourceProvenance.replace("b".repeat(40), "BAD")) }
+ assertFails {
+ SourceProvenance.parse(
+ sourceProvenance + "\n[[import]]\ncomponent = \"domain\"\ncommit = \"${"d".repeat(40)}\"\n",
+ )
+ }
+ assertTrue(
+ SourceProvenance.parse(sourceProvenance.replace("b".repeat(40), "d".repeat(40))).digest != canonical.digest,
+ )
}
@Test
@@ -157,4 +214,12 @@ class BuildContractsTest {
component = "domain"
commit = "${"b".repeat(40)}"
""".trimIndent() + "\n"
+
+ private fun String.replaceCoordinate(
+ key: String,
+ replacement: String,
+ ): String =
+ lineSequence().joinToString("\n") { line ->
+ if (line.substringBefore('=', missingDelimiterValue = "") == key) "$key=$replacement" else line
+ }
}
diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleRootPlugin.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleRootPlugin.kt
@@ -49,7 +49,7 @@ public class HarvestCircleRootPlugin : Plugin<Project> {
task.description = "Validates forge-agnostic verification lanes and least-privilege policy."
task.policyFile.set(verificationLanesFile)
task.productManifestFile.set(productCoordinatesFile)
- task.repositoryRoot.set(target.layout.projectDirectory)
+ task.makefileFile.set(target.layout.projectDirectory.file("Makefile"))
}
target.providers.environmentVariable("EXT_BUILD_GRADLE_BUILD_DIR").orNull?.let { outputRoot ->
target.layout.buildDirectory.set(target.file(outputRoot).resolve("root"))
diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/ProductCoordinatesTask.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/ProductCoordinatesTask.kt
@@ -36,33 +36,6 @@ abstract class VerifyProductCoordinates : DefaultTask() {
val source = manifestFile.get().asFile.readText()
val coordinates = ProductCoordinates.parse(source)
check(coordinates.digest.matches(Regex("[0-9a-f]{64}")))
- val equivalentSources =
- listOf(
- source.replace("\n", "\r\n"),
- source.trimEnd(),
- "# comment\n$source",
- source.lineSequence().joinToString("\n") { line ->
- if (line.isBlank() || line.startsWith('#')) line else line.replaceFirst("=", " = ")
- },
- )
- equivalentSources.forEach { equivalent ->
- check(ProductCoordinates.parse(equivalent).digest == coordinates.digest)
- }
- check(runCatching { ProductCoordinates.parse("\uFEFF$source") }.isFailure)
- check(runCatching { ProductCoordinates.parse(source + "\nschema=${ProductCoordinates.schema}") }.isFailure)
- check(runCatching { ProductCoordinates.parse(source + "\nunknown=value") }.isFailure)
- check(runCatching { ProductCoordinates.parse(source.substringAfter('\n')) }.isFailure)
- check(runCatching { ProductCoordinates.parse(source.replaceCoordinate("product.slug", "INVALID")) }.isFailure)
- check(
- runCatching {
- ProductCoordinates.parse(source.replaceCoordinate("database.filename", "../other.sqlite3"))
- }.isFailure,
- )
- validCoordinateMutations.forEach { (key, replacement) ->
- val mutated = ProductCoordinates.parse(source.replaceCoordinate(key, replacement))
- check(mutated[key] == replacement)
- check(mutated.digest != coordinates.digest)
- }
val uniFfiConfig = uniFfiConfigFile.get().asFile.readText()
check(
@@ -77,65 +50,10 @@ abstract class VerifyProductCoordinates : DefaultTask() {
)
val baseline = FfiCompatibilityBaseline.load(ffiBaselineFile.get().asFile)
- val baselineSource = ffiBaselineFile.get().asFile.readText()
- check(runCatching { FfiCompatibilityBaseline.parse(baselineSource + "\nunknown=value") }.isFailure)
- check(runCatching { FfiCompatibilityBaseline.parse(baselineSource.substringAfter('\n')) }.isFailure)
- check(runCatching { FfiCompatibilityBaseline.parse("\uFEFF$baselineSource") }.isFailure)
- check(
- runCatching {
- FfiCompatibilityBaseline.parse(
- baselineSource.replace(
- Regex("(?m)^contract\\.hash=.*$"),
- "contract.hash=malformed",
- ),
- )
- }.isFailure,
- )
- check(
- runCatching {
- FfiCompatibilityBaseline.parse(
- baselineSource.replace(
- Regex("(?m)^package\\.version=.*$"),
- "package.version=invalid",
- ),
- )
- }.isFailure,
- )
- check(
- runCatching {
- FfiCompatibilityBaseline.parse(
- baselineSource + "\ncontract.id=harvestcircle-desktop-ffi-v4",
- )
- }.isFailure,
- )
check(baseline["product.coordinate_digest"] == coordinates.digest)
- val provenanceSource = sourceProvenanceFile.get().asFile.readText()
- val provenance = SourceProvenance.parse(provenanceSource)
+ val provenance = SourceProvenance.load(sourceProvenanceFile.get().asFile)
check(baseline["source.provenance_digest"] == provenance.digest)
check(provenance.foundationBaseline == baseline["source.foundation_baseline"])
- val equivalentProvenance =
- listOf(
- provenanceSource.replace("\n", "\r\n"),
- provenanceSource.trimEnd(),
- "# comment\n$provenanceSource",
- provenanceSource.replace(
- "component = \"domain\"\ncommit = \"a4d7deebec3e2ce2c1daa455de6d79857839aed0\"",
- "commit = \"a4d7deebec3e2ce2c1daa455de6d79857839aed0\"\ncomponent = \"domain\"",
- ),
- )
- equivalentProvenance.forEach { equivalent ->
- check(SourceProvenance.parse(equivalent).digest == provenance.digest)
- }
- check(runCatching { SourceProvenance.parse("\uFEFF$provenanceSource") }.isFailure)
- check(runCatching { SourceProvenance.parse("unknown = \"value\"\n$provenanceSource") }.isFailure)
- check(
- SourceProvenance.parse(
- provenanceSource.replace(
- "a4d7deebec3e2ce2c1daa455de6d79857839aed0",
- "b4d7deebec3e2ce2c1daa455de6d79857839aed0",
- ),
- ).digest != provenance.digest,
- )
val nativeCompatibility = nativeCompatibilityFile.get().asFile.readText()
check(nativeCompatibility.contains("NativeCompatibilityExpectations as Expected"))
listOf(
@@ -147,33 +65,4 @@ abstract class VerifyProductCoordinates : DefaultTask() {
).forEach { key -> check(!nativeCompatibility.contains(baseline[key])) }
}
- private fun String.replaceCoordinate(
- key: String,
- replacement: String,
- ): String =
- lineSequence().joinToString("\n") { line ->
- if (line.substringBefore('=', missingDelimiterValue = "") == key) "$key=$replacement" else line
- }
-
- private val validCoordinateMutations =
- linkedMapOf(
- "product.name" to "Harvest Circle Test",
- "product.slug" to "harvestcircle_test",
- "kotlin.root_namespace" to "org.example",
- "desktop.application_id" to "org.example.desktop",
- "desktop.bundle_id" to "org.example.bundle",
- "desktop.main_class" to "org.example.MainKt",
- "ffi.kotlin_package" to "org.example.ffi",
- "ffi.cdylib_name" to "example_ffi",
- "database.qualifier" to "com",
- "database.organization" to "example",
- "database.application" to "test",
- "database.filename" to "example.sqlite3",
- "keyring.service" to "org.example.desktop.nostr",
- "environment.prefix" to "EXAMPLE_",
- "vendor.name" to "Example Cooperative",
- "copyright.notice" to "Copyright Example contributors",
- ).also { mutations ->
- check(mutations.size + 1 == ProductCoordinates.requiredKeys.size)
- }
}
diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/VerificationLanes.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/VerificationLanes.kt
@@ -2,10 +2,8 @@ package org.harvestcircle.buildlogic.plugins.tasks
import org.harvestcircle.buildlogic.contracts.ProductCoordinates
import org.gradle.api.DefaultTask
-import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
-import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
@@ -73,8 +71,9 @@ abstract class VerifyVerificationLanes : DefaultTask() {
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val productManifestFile: RegularFileProperty
- @get:Internal
- abstract val repositoryRoot: DirectoryProperty
+ @get:InputFile
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val makefileFile: RegularFileProperty
@TaskAction
fun verify() {
@@ -83,19 +82,7 @@ abstract class VerifyVerificationLanes : DefaultTask() {
ProductCoordinates.load(productManifestFile.get().asFile)["environment.prefix"]
val policy = VerificationLanes.parse(source, environmentPrefix)
check(policy.size == 24)
- check(runCatching { VerificationLanes.parse(source + "source.workflow=forbidden", environmentPrefix) }.isFailure)
- check(
- runCatching {
- VerificationLanes.parse(source.replace("credentials=none", "credentials=all"), environmentPrefix)
- }.isFailure,
- )
- check(
- runCatching {
- VerificationLanes.parse(source.replace("release.mode=governed", "release.mode=standalone"), environmentPrefix)
- }.isFailure,
- )
- val root = repositoryRoot.get().asFile.toPath()
- val makefile = root.resolve("Makefile").toFile().readText()
+ val makefile = makefileFile.get().asFile.readText()
policy.filterKeys { it.endsWith(".command") }.forEach { (key, command) ->
val target = command.removePrefix("make ")
check(command == "make $target" && Regex("(?m)^${Regex.escape(target)}:").containsMatchIn(makefile)) {
diff --git a/build-logic/plugins/src/test/kotlin/org/harvestcircle/buildlogic/plugins/tasks/VerificationLanesTest.kt b/build-logic/plugins/src/test/kotlin/org/harvestcircle/buildlogic/plugins/tasks/VerificationLanesTest.kt
@@ -0,0 +1,54 @@
+package org.harvestcircle.buildlogic.plugins.tasks
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFails
+
+class VerificationLanesTest {
+ @Test
+ fun policyRequiresTheExactLeastPrivilegeLaneMap() {
+ val parsed = VerificationLanes.parse(policy, "HARVESTCIRCLE_")
+
+ assertEquals(24, parsed.size)
+ assertFails { VerificationLanes.parse(policy + "source.workflow=forbidden\n", "HARVESTCIRCLE_") }
+ assertFails { VerificationLanes.parse(policy.replaceFirst("schema=", "schema"), "HARVESTCIRCLE_") }
+ assertFails { VerificationLanes.parse(policy.replace("schema=harvestcircle.verification-lanes.v2\n", ""), "HARVESTCIRCLE_") }
+ assertFails { VerificationLanes.parse(policy + "schema=harvestcircle.verification-lanes.v2\n", "HARVESTCIRCLE_") }
+ assertFails { VerificationLanes.parse(policy.replace("source.credentials=none", "source.credentials=all"), "HARVESTCIRCLE_") }
+ assertFails { VerificationLanes.parse(policy.replace("release.mode=governed", "release.mode=standalone"), "HARVESTCIRCLE_") }
+ assertFails {
+ VerificationLanes.parse(
+ policy.replace("HARVESTCIRCLE_BUILD_SOURCE_COMMIT", "BUILD_SOURCE_COMMIT"),
+ "HARVESTCIRCLE_",
+ )
+ }
+ }
+
+ private val policy =
+ """
+ schema=harvestcircle.verification-lanes.v2
+ orchestration=explicit-make-modes
+ source.standalone.command=make source-check
+ source.governed.command=make governed-source-check
+ source.credentials=none
+ integration.standalone.command=make integration-check
+ integration.governed.command=make governed-integration-check
+ integration.credentials=none
+ package.standalone.command=make host-package-check
+ package.governed.command=make governed-package-check
+ package.runners=linux,macos,windows
+ package.credentials=none
+ provenance.commit=HARVESTCIRCLE_BUILD_SOURCE_COMMIT
+ provenance.dirty=HARVESTCIRCLE_BUILD_SOURCE_DIRTY
+ provenance.radroots=HARVESTCIRCLE_BUILD_RADROOTS_REVISION
+ provenance.epoch=SOURCE_DATE_EPOCH
+ signing.command=make signing-check
+ signing.runner=macos
+ signing.credentials=signing
+ notarization.command=make notarization-check
+ notarization.runner=macos
+ notarization.credentials=notarization
+ release.command=make release-check
+ release.mode=governed
+ """.trimIndent() + "\n"
+}