app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
Mbuild-logic/contracts/src/test/kotlin/org/harvestcircle/buildlogic/contracts/BuildContractsTest.kt | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleRootPlugin.kt | 2+-
Mbuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/ProductCoordinatesTask.kt | 113+------------------------------------------------------------------------------
Mbuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/VerificationLanes.kt | 21++++-----------------
Abuild-logic/plugins/src/test/kotlin/org/harvestcircle/buildlogic/plugins/tasks/VerificationLanesTest.kt | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
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" +}