commit 5186bd45d9a368a8ae28c76d36bab45569325a26
parent dc8d3c8995f5665f0ed58b63328f46271babbf2f
Author: triesap <tyson@radroots.org>
Date: Mon, 10 Aug 2026 19:22:55 +0000
ci: return workflow authority to local orchestration
- remove GitHub workflow definitions from the standalone capsule
- make portable Make targets the verification-lane authority
- fail closed on every tracked workflow root inside the OSS repository
- align public security and architecture contracts with local execution
Diffstat:
12 files changed, 57 insertions(+), 209 deletions(-)
diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
@@ -1,57 +0,0 @@
-name: Package verification
-
-on:
- pull_request:
- push:
- branches:
- - dev
- workflow_dispatch:
-
-permissions:
- contents: read
-
-jobs:
- package:
- strategy:
- fail-fast: false
- matrix:
- os:
- - ubuntu-latest
- - macos-latest
- - windows-latest
- runs-on: ${{ matrix.os }}
- steps:
- - name: Check out source
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- with:
- fetch-depth: 0
- persist-credentials: false
- - name: Install Java 21
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
- with:
- distribution: temurin
- java-version: "21"
- - name: Install Rust toolchain
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772
- with:
- toolchain: 1.97.1
- components: clippy,rustfmt
- - name: Install cargo-deny
- uses: taiki-e/install-action@7f4eb899022d8fe70b20c4f3de697aa85c309026 # v2
- with:
- tool: cargo-deny
- - name: Install Windows package tools
- if: runner.os == 'Windows'
- shell: pwsh
- run: choco install make wixtoolset --no-progress --yes
- - name: Export source provenance
- shell: bash
- run: |
- radroots_revision=$(sed -n 's/^canonical_radroots_revision = "\([0-9a-f]\{40\}\)"$/\1/p' core/provenance/studio-import-v1.toml)
- test ${#radroots_revision} -eq 40
- echo "HARVESTCIRCLE_BUILD_SOURCE_COMMIT=$GITHUB_SHA" >> "$GITHUB_ENV"
- echo "HARVESTCIRCLE_BUILD_SOURCE_DIRTY=false" >> "$GITHUB_ENV"
- echo "HARVESTCIRCLE_BUILD_RADROOTS_REVISION=$radroots_revision" >> "$GITHUB_ENV"
- echo "SOURCE_DATE_EPOCH=$(git show -s --format=%ct "$GITHUB_SHA")" >> "$GITHUB_ENV"
- - name: Verify host package
- run: make package-check
diff --git a/.github/workflows/source.yml b/.github/workflows/source.yml
@@ -1,46 +0,0 @@
-name: Source verification
-
-on:
- pull_request:
- push:
- branches:
- - dev
- workflow_dispatch:
-
-permissions:
- contents: read
-
-jobs:
- source:
- runs-on: ubuntu-latest
- steps:
- - name: Check out source
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- with:
- fetch-depth: 0
- persist-credentials: false
- - name: Install Java 21
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
- with:
- distribution: temurin
- java-version: "21"
- - name: Install Rust toolchain
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772
- with:
- toolchain: 1.97.1
- components: clippy,rustfmt
- - name: Install cargo-deny
- uses: taiki-e/install-action@7f4eb899022d8fe70b20c4f3de697aa85c309026 # v2
- with:
- tool: cargo-deny
- - name: Export source provenance
- shell: bash
- run: |
- radroots_revision=$(sed -n 's/^canonical_radroots_revision = "\([0-9a-f]\{40\}\)"$/\1/p' core/provenance/studio-import-v1.toml)
- test ${#radroots_revision} -eq 40
- echo "HARVESTCIRCLE_BUILD_SOURCE_COMMIT=$GITHUB_SHA" >> "$GITHUB_ENV"
- echo "HARVESTCIRCLE_BUILD_SOURCE_DIRTY=false" >> "$GITHUB_ENV"
- echo "HARVESTCIRCLE_BUILD_RADROOTS_REVISION=$radroots_revision" >> "$GITHUB_ENV"
- echo "SOURCE_DATE_EPOCH=$(git show -s --format=%ct "$GITHUB_SHA")" >> "$GITHUB_ENV"
- - name: Verify source
- run: make source-check
diff --git a/AGENTS.md b/AGENTS.md
@@ -58,16 +58,17 @@ local artifacts, absolute host paths, or an enclosing monorepo layout.
Local product crates are workspace path dependencies; shared Radroots
packages remain immutable public Git dependencies.
- The repository tracks durable public product specifications, decisions,
- contributor and security guidance, qualification evidence, and thin CI
- wrappers. Approved public surfaces are `README.md`, `NOTICE`,
+ contributor and security guidance, and qualification evidence. Approved
+ public surfaces are `README.md`, `NOTICE`,
`CONTRIBUTING.md`, `SECURITY.md`, `LICENSE`, `LICENSES/**`,
`spec/harvestcircle_mvp_v1/**`, `docs/decisions/**`,
- `docs/qualification/**`, and `.github/workflows/{source,package}.yml`.
- These roots are inspected by the same namespace, secret, generated-output,
- credential, and symlink rules as source code. Internal handoffs, RCLDs,
- migration narratives, and execution records remain parent-owned. Other
- `docs/**`, `spec/**`, `.github/**`, and all `.act/**` paths are forbidden.
- CI must remain a thin wrapper around repository-owned Make targets.
+ and `docs/qualification/**`. These roots are inspected by the same
+ namespace, secret, generated-output, credential, and symlink rules as source
+ code. Internal handoffs, RCLDs, migration narratives, and execution records
+ remain parent-owned. Other `docs/**` and `spec/**` paths are forbidden. All
+ `.github/**` and `.act/**` paths are forbidden; local workflow orchestration
+ belongs to the consuming monorepo's governed `.act/**` surface and must call
+ this capsule's standalone Make targets.
Generated UniFFI Kotlin and native libraries are derived build output. Change
the local canonical Rust producer contract/generator first, regenerate into
@@ -128,8 +129,8 @@ blockers exactly.
Verify exact manifests/lock agreement, generated binding and native artifact
freshness when affected, compatibility and architecture guards,
license/source policy, zero forbidden roots, `git diff --check`, and final
-status and diff. Do not treat parent-only workflow proof as a substitute for
-standalone repository validation.
+status and diff. Parent local-`act` proof is integration evidence only: it must
+execute, and never replace, the standalone repository validation commands.
## Git and external gates
diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/architecture/ProductNamespaceGuardTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/architecture/ProductNamespaceGuardTest.kt
@@ -127,9 +127,6 @@ class ProductNamespaceGuardTest {
if (relative == "spec/harvestcircle_mvp_v1/UI_SURFACE_MAP.md") {
inspected = inspected.replace("round_${legacyProduct}_screen", "")
}
- if (relative == ".github/workflows/source.yml" || relative == ".github/workflows/package.yml") {
- inspected = inspected.replace(provenanceException, "")
- }
if (inspected.lowercase().contains(legacyProduct)) {
add("$relative: legacy product name in tracked text")
}
diff --git a/build.gradle.kts b/build.gradle.kts
@@ -14,7 +14,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 verificationLanesFile = layout.projectDirectory.file("config/verification/lanes-v2.properties")
val legacyProduct = "stu" + "dio"
val verifyProductCoordinates by tasks.registering(VerifyProductCoordinates::class) {
diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/FoundationBoundaryAudit.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/FoundationBoundaryAudit.kt
@@ -192,9 +192,6 @@ private class FoundationBoundaryAudit(
if (relative == "spec/harvestcircle_mvp_v1/UI_SURFACE_MAP.md") {
inspected = inspected.replace("round_${legacyProduct}_screen", "")
}
- if (relative == ".github/workflows/source.yml" || relative == ".github/workflows/package.yml") {
- inspected = inspected.replace("core/provenance/$legacyProduct-import-v1.toml", "")
- }
if (inspected.lowercase().contains(legacyProduct)) {
findings += "$relative: legacy product name outside the exact provenance allowlist"
}
@@ -254,8 +251,6 @@ private class FoundationBoundaryAudit(
"docs/decisions/ADR-0008-public-specs-and-ci.md",
"docs/decisions/ADR-0009-canonical-manifest-digests.md",
"docs/decisions/ADR-0010-gap-aware-snapshot-delivery.md",
- ".github/workflows/source.yml",
- ".github/workflows/package.yml",
)
(requiredPublicFiles - inventory.toSet()).sorted().forEach { relative ->
findings += "$relative: required public repository file is missing"
@@ -320,14 +315,10 @@ private class FoundationBoundaryAudit(
"docs",
"docs/decisions",
"docs/qualification",
- ".github",
- ".github/workflows",
) ||
normalized.startsWith("spec/harvestcircle_mvp_v1/") ||
normalized.startsWith("docs/decisions/") ||
- normalized.startsWith("docs/qualification/") ||
- normalized == ".github/workflows/source.yml" ||
- normalized == ".github/workflows/package.yml"
+ normalized.startsWith("docs/qualification/")
private fun isText(relative: String): Boolean {
val path = Path.of(relative)
diff --git a/buildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt b/buildSrc/src/main/kotlin/org/harvestcircle/gradle/VerificationLanes.kt
@@ -12,17 +12,13 @@ import org.gradle.api.tasks.TaskAction
object VerificationLanes {
private fun expected(environmentPrefix: String) =
linkedMapOf(
- "schema" to "harvestcircle.verification-lanes.v1",
- "orchestration" to "github-actions",
+ "schema" to "harvestcircle.verification-lanes.v2",
+ "orchestration" to "standalone-make",
"source.command" to "make source-check",
- "source.runner" to "linux",
- "source.workflow" to ".github/workflows/source.yml",
- "source.permissions" to "contents:read",
+ "source.runner" to "host",
"source.credentials" to "none",
"package.command" to "make package-check",
"package.runners" to "linux,macos,windows",
- "package.workflow" to ".github/workflows/package.yml",
- "package.permissions" to "contents:read",
"package.credentials" to "none",
"provenance.commit" to environmentPrefix + "BUILD_SOURCE_COMMIT",
"provenance.dirty" to environmentPrefix + "BUILD_SOURCE_DIRTY",
@@ -30,11 +26,9 @@ object VerificationLanes {
"provenance.epoch" to "SOURCE_DATE_EPOCH",
"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",
)
@@ -81,13 +75,8 @@ abstract class VerifyVerificationLanes : DefaultTask() {
val environmentPrefix =
ProductCoordinates.load(productManifestFile.get().asFile)["environment.prefix"]
val policy = VerificationLanes.parse(source, environmentPrefix)
- check(policy.size == 24)
- check(runCatching { VerificationLanes.parse(source + "source.permissions=write", environmentPrefix) }.isFailure)
- check(
- runCatching {
- VerificationLanes.parse(source.replace("contents:read", "contents:write"), environmentPrefix)
- }.isFailure,
- )
+ check(policy.size == 18)
+ check(runCatching { VerificationLanes.parse(source + "source.workflow=forbidden", environmentPrefix) }.isFailure)
check(
runCatching {
VerificationLanes.parse(source.replace("credentials=none", "credentials=all"), environmentPrefix)
@@ -95,46 +84,20 @@ abstract class VerifyVerificationLanes : DefaultTask() {
)
check(
runCatching {
- VerificationLanes.parse(source.replace("source.runner=linux", "source.runner=macos"), environmentPrefix)
+ VerificationLanes.parse(source.replace("source.runner=host", "source.runner=remote"), environmentPrefix)
}.isFailure,
)
val root = repositoryRoot.get().asFile.toPath()
- val sourceWorkflow = root.resolve(policy.getValue("source.workflow")).toFile().readText()
- val packageWorkflow = root.resolve(policy.getValue("package.workflow")).toFile().readText()
- verifyWorkflow(sourceWorkflow, policy.getValue("source.command"))
- verifyWorkflow(packageWorkflow, policy.getValue("package.command"))
- check(sourceWorkflow.contains("runs-on: ubuntu-latest"))
- listOf("ubuntu-latest", "macos-latest", "windows-latest").forEach { runner ->
- check(packageWorkflow.contains("- $runner")) { "Package workflow is missing $runner" }
- }
- listOf(
- policy.getValue("provenance.commit"),
- policy.getValue("provenance.dirty"),
- policy.getValue("provenance.radroots"),
- policy.getValue("provenance.epoch"),
- ).forEach { variable ->
- check(sourceWorkflow.contains(variable)) { "Source workflow is missing provenance variable $variable" }
- check(packageWorkflow.contains(variable)) { "Package workflow is missing provenance variable $variable" }
- }
- }
-
- private fun verifyWorkflow(
- source: String,
- command: String,
- ) {
- check(Regex("(?m)^permissions:\\s*\\n\\s{2}contents: read$").containsMatchIn(source)) {
- "Workflow permissions must be contents: read"
- }
- check(source.contains("run: $command")) { "Workflow does not invoke $command" }
- check(source.contains("persist-credentials: false")) { "Checkout credentials must not persist" }
- val actionPins = Regex("(?m)^\\s*uses:\\s+[^@\\s]+@([0-9a-f]{40})(?:\\s+#.*)?$").findAll(source).toList()
- check(actionPins.isNotEmpty()) { "Workflow does not use any pinned actions" }
- check(source.lineSequence().filter { "uses:" in it }.count() == actionPins.size) {
- "Every workflow action must use a full immutable commit SHA"
+ val makefile = root.resolve("Makefile").toFile().readText()
+ listOf("source.command", "package.command", "signing.command", "notarization.command").forEach { key ->
+ val command = policy.getValue(key)
+ val target = command.removePrefix("make ")
+ check(command == "make $target" && Regex("(?m)^${Regex.escape(target)}:").containsMatchIn(makefile)) {
+ "Verification lane $key does not name a standalone Make target"
+ }
}
- val forbidden = listOf("contents: write", "id-token: write", "pull-requests: write", "secrets.", "publish", "deploy")
- forbidden.forEach { token ->
- check(!source.contains(token, ignoreCase = true)) { "Workflow contains forbidden capability $token" }
+ check(policy.values.none { ".github/" in it || ".act/" in it }) {
+ "Standalone verification policy must not reference an orchestration root"
}
}
}
diff --git a/config/verification/lanes-v1.properties b/config/verification/lanes-v1.properties
@@ -1,24 +0,0 @@
-schema=harvestcircle.verification-lanes.v1
-orchestration=github-actions
-source.command=make source-check
-source.runner=linux
-source.workflow=.github/workflows/source.yml
-source.permissions=contents:read
-source.credentials=none
-package.command=make package-check
-package.runners=linux,macos,windows
-package.workflow=.github/workflows/package.yml
-package.permissions=contents:read
-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.permissions=contents:read
-signing.credentials=signing
-notarization.command=make notarization-check
-notarization.runner=macos
-notarization.permissions=contents:read
-notarization.credentials=notarization
diff --git a/config/verification/lanes-v2.properties b/config/verification/lanes-v2.properties
@@ -0,0 +1,18 @@
+schema=harvestcircle.verification-lanes.v2
+orchestration=standalone-make
+source.command=make source-check
+source.runner=host
+source.credentials=none
+package.command=make 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
diff --git a/docs/decisions/ADR-0008-public-specs-and-ci.md b/docs/decisions/ADR-0008-public-specs-and-ci.md
@@ -1,11 +1,16 @@
-# ADR-0008: Public specs and CI are repository requirements
+# ADR-0008: Public specs and local verification are repository requirements
Status: Accepted
HarvestCircle is an open-source, spec-anchored project intended for public
review and an OpenSats application.
-Durable specs, decisions, qualification evidence, and CI wrappers belong in
-the repository.
+Durable specs, decisions, and qualification evidence belong in the repository.
+The standalone capsule owns portable Make, Gradle, and Cargo verification
+commands, but it does not own workflow definitions under `.github/**` or
+`.act/**`.
-Make/Gradle/Cargo remain the implementation authority. CI invokes them.
+The consuming monorepo may invoke those commands through governed local-only
+workflows under its root `.act/**` surface. Those workflows add orchestration,
+not build behavior, and are not a substitute for running the standalone
+commands directly.
diff --git a/spec/harvestcircle_mvp_v1/ACCEPTANCE_CRITERIA.md b/spec/harvestcircle_mvp_v1/ACCEPTANCE_CRITERIA.md
@@ -2,7 +2,7 @@
The foundation is complete when:
-- public specs/governance and CI exist;
+- public specs/governance and portable standalone verification exist;
- product/provenance digests are canonical;
- coordinate values have one authority;
- Kotlin compatibility expectations are generated;
diff --git a/spec/harvestcircle_mvp_v1/SECURITY_AND_PRIVACY.md b/spec/harvestcircle_mvp_v1/SECURITY_AND_PRIVACY.md
@@ -10,4 +10,4 @@
- no silent provider fallback;
- no private event through public sinks;
- unknown protocol versions fail closed;
-- public CI uses least privilege.
+- local workflow orchestration is parent-owned, credential-free, and fail-closed.