commit b6ab1d4101763281e49762924d1f6b2ac8db2384
parent 1b3f190e3c463fb7eecf46aaf93727057f1ab057
Author: triesap <tyson@radroots.org>
Date: Mon, 10 Aug 2026 03:47:57 +0000
ci: publish repository-owned verification lanes
- split source, package, signing, and notarization readiness behind Make and Gradle
- validate runner, command, permission, and credential isolation in machine policy
- keep external orchestration thin and forge agnostic under the forbidden-root invariant
- verify source, bindings, licenses, host packaging, and both integration lanes
Diffstat:
5 files changed, 122 insertions(+), 4 deletions(-)
diff --git a/Makefile b/Makefile
@@ -5,10 +5,10 @@ CARGO ?= cargo
CARGO_MANIFEST := core/Cargo.toml
EXTBUILD ?= $(if $(shell cargo extbuild --version 2>/dev/null),cargo extbuild run --)
-.PHONY: help doctor lock metadata format format-fix lint test check build bindings dev run audit licenses package release-check clean
+.PHONY: help doctor lock metadata format format-fix lint test check build bindings dev run audit licenses package source-check package-check signing-check notarization-check release-check clean
help:
- @printf '%s\n' doctor lock metadata format format-fix lint test check build bindings dev run audit licenses package release-check clean
+ @printf '%s\n' doctor lock metadata format format-fix lint test check build bindings dev run audit licenses package source-check package-check signing-check notarization-check release-check clean
doctor:
$(if $(strip $(EXTBUILD)),cargo extbuild doctor,@:)
@@ -66,6 +66,18 @@ licenses: doctor
package: check
$(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:verifyHostPackage
+source-check: check bindings licenses
+ $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:sourceReadiness
+
+package-check: source-check
+ $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:packageReadiness
+
+signing-check: doctor
+ $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:signingReadiness
+
+notarization-check: doctor
+ $(EXTBUILD) $(GRADLE) --no-daemon :app:desktop:notarizationReadiness
+
release-check: doctor
$(EXTBUILD) $(CARGO) audit --file core/Cargo.lock
$(EXTBUILD) $(CARGO) deny --manifest-path $(CARGO_MANIFEST) check --config core/deny.toml advisories licenses sources
diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts
@@ -895,9 +895,21 @@ val verifyReleaseBuildProvenance by tasks.registering(VerifyReleaseBuildProvenan
radrootsRevision.set(buildRadrootsRevision)
sourceDateEpoch.set(buildSourceDateEpoch)
}
+val sourceReadiness by tasks.registering {
+ dependsOn(":verifyProductCoordinates", ":verifyVerificationLanes", ":app:shared:check", "check", verifyUniFfiBindings)
+}
+val packageReadiness by tasks.registering {
+ dependsOn(verifyHostPackage, verifyReleaseBuildProvenance)
+}
+val signingReadiness by tasks.registering {
+ dependsOn(verifyMacOsDeveloperIdSignature)
+}
+val notarizationReadiness by tasks.registering {
+ dependsOn(verifyMacOsNotarization)
+}
tasks.register("releaseReadiness") {
- dependsOn("checkLicense", "dependencyCheckAnalyze", verifyHostPackage, verifyReleaseBuildProvenance)
+ dependsOn("checkLicense", "dependencyCheckAnalyze", sourceReadiness, packageReadiness)
if (isMacOsHost) {
- dependsOn(verifyMacOsDeveloperIdSignature, verifyMacOsNotarization)
+ dependsOn(signingReadiness, notarizationReadiness)
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
@@ -1,4 +1,5 @@
import org.harvestcircle.gradle.VerifyProductCoordinates
+import org.harvestcircle.gradle.VerifyVerificationLanes
plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
@@ -10,6 +11,7 @@ plugins {
val productCoordinatesFile = layout.projectDirectory.file("config/product/harvestcircle-v1.properties")
val ffiCompatibilityBaselineFile =
layout.projectDirectory.file("core/compatibility/harvestcircle-ffi-v4.properties")
+val verificationLanesFile = layout.projectDirectory.file("config/verification/lanes-v1.properties")
val legacyProduct = "stu" + "dio"
val verifyProductCoordinates by tasks.registering(VerifyProductCoordinates::class) {
@@ -30,6 +32,12 @@ val verifyProductCoordinates by tasks.registering(VerifyProductCoordinates::clas
)
}
+val verifyVerificationLanes by tasks.registering(VerifyVerificationLanes::class) {
+ group = "verification"
+ description = "Validates forge-agnostic verification lanes and least-privilege policy."
+ policyFile.set(verificationLanesFile)
+}
+
providers.environmentVariable("EXT_BUILD_GRADLE_BUILD_DIR").orNull?.let { extBuildGradleRoot ->
layout.buildDirectory.set(file(extBuildGradleRoot).resolve("root"))
}
diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt
@@ -0,0 +1,68 @@
+package org.harvestcircle.gradle
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+
+object VerificationLanes {
+ private val expected =
+ linkedMapOf(
+ "schema" to "harvestcircle.verification-lanes.v1",
+ "orchestration" to "external-forge-agnostic",
+ "source.command" to "make source-check",
+ "source.runner" to "linux",
+ "source.permissions" to "contents:read",
+ "source.credentials" to "none",
+ "package.command" to "make package-check",
+ "package.runners" to "linux,macos,windows",
+ "package.permissions" to "contents:read",
+ "package.credentials" to "none",
+ "signing.command" to "make signing-check",
+ "signing.runner" to "macos",
+ "signing.permissions" to "contents:read",
+ "signing.credentials" to "signing",
+ "notarization.command" to "make notarization-check",
+ "notarization.runner" to "macos",
+ "notarization.permissions" to "contents:read",
+ "notarization.credentials" to "notarization",
+ )
+
+ fun parse(source: String): Map<String, String> {
+ val parsed = linkedMapOf<String, String>()
+ source.trimEnd('\n', '\r').lineSequence().forEachIndexed { index, raw ->
+ val line = raw.trim()
+ require(line.isNotEmpty() && !line.startsWith('#')) {
+ "Verification lane policy contains an empty or comment line at ${index + 1}"
+ }
+ val separator = line.indexOf('=')
+ require(separator > 0 && separator < line.lastIndex && line.indexOf('=', separator + 1) == -1) {
+ "Verification lane policy contains malformed syntax at ${index + 1}"
+ }
+ val key = line.substring(0, separator)
+ val value = line.substring(separator + 1)
+ require(parsed.put(key, value) == null) { "Duplicate verification lane key: $key" }
+ }
+ require(parsed.keys == expected.keys) { "Verification lane keys do not match the authority" }
+ require(parsed == expected) { "Verification lane values do not match the authority" }
+ return parsed
+ }
+}
+
+abstract class VerifyVerificationLanes : DefaultTask() {
+ @get:InputFile
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val policyFile: RegularFileProperty
+
+ @TaskAction
+ fun verify() {
+ val source = policyFile.get().asFile.readText()
+ check(VerificationLanes.parse(source).size == 18)
+ check(runCatching { VerificationLanes.parse(source + "source.permissions=write") }.isFailure)
+ check(runCatching { VerificationLanes.parse(source.replace("contents:read", "contents:write")) }.isFailure)
+ check(runCatching { VerificationLanes.parse(source.replace("credentials=none", "credentials=all")) }.isFailure)
+ check(runCatching { VerificationLanes.parse(source.replace("source.runner=linux", "source.runner=macos")) }.isFailure)
+ }
+}
diff --git a/config/verification/lanes-v1.properties b/config/verification/lanes-v1.properties
@@ -0,0 +1,18 @@
+schema=harvestcircle.verification-lanes.v1
+orchestration=external-forge-agnostic
+source.command=make source-check
+source.runner=linux
+source.permissions=contents:read
+source.credentials=none
+package.command=make package-check
+package.runners=linux,macos,windows
+package.permissions=contents:read
+package.credentials=none
+signing.command=make signing-check
+signing.runner=macos
+signing.permissions=contents:read
+signing.credentials=signing
+notarization.command=make notarization-check
+notarization.runner=macos
+notarization.permissions=contents:read
+notarization.credentials=notarization