Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/staging-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ jobs:
# Seal those directory inodes before this job exercises the same
# privileged recipe boundary as publication.
- name: Seal conventional host runtime ownership
if: ${{ needs.change-scope.outputs.package_publish_flow_changed == 'true' }}
run: >-
/usr/bin/python3
scripts/prepare-homebrew-recipe-host-runtime.py
Expand All @@ -264,6 +265,7 @@ jobs:
# reviewed Homebrew source. Give it an immutable source explicitly so
# runner-image Homebrew state cannot decide what the test executes.
- name: Checkout exact Homebrew lifecycle source
if: ${{ needs.change-scope.outputs.package_publish_flow_changed == 'true' }}
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
Expand All @@ -288,12 +290,14 @@ jobs:
run: bash scripts/dev-shell.sh true

- name: Install JavaScript dependencies for Homebrew preflight
if: ${{ needs.change-scope.outputs.package_publish_flow_changed == 'true' }}
run: |
bash scripts/dev-shell.sh bash -c '
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci --no-audit --no-fund
'

- name: Validate Homebrew publisher trust contract
if: ${{ needs.change-scope.outputs.package_publish_flow_changed == 'true' }}
run: |
bash scripts/dev-shell.sh env \
KANDELO_HOMEBREW_SOURCE_REPOSITORY="$GITHUB_WORKSPACE/homebrew-lifecycle-source" \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Scope the Homebrew Publisher Suite Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Stop running the complete Homebrew publisher integration suite for pull requests that do not change publisher policy.

**Architecture:** Reuse the existing `package_publish_flow_changed` output as the single routing authority. Apply one identical GitHub Actions condition to the publisher-only setup and validation steps, and enforce it with mutation-style Ruby contract tests.

**Tech Stack:** GitHub Actions YAML, Ruby workflow contract tests, Bash verification through `scripts/dev-shell.sh`.

## Global Constraints

- Formula, package, ABI, request, candidate, and bottle identities must remain unchanged.
- Mixed changes that touch publisher policy must run the complete suite.
- The pinned Homebrew source and explicit source environment remain mandatory when the suite runs.
- No new path classifier or compatibility fallback is permitted.

---

### Task 1: Enforce publisher-only preflight routing

**Files:**
- Modify: `scripts/test-homebrew-publisher-lifecycle-source.rb`
- Modify: `scripts/check-homebrew-publish-workflow-trust.rb`
- Modify: `.github/workflows/staging-build.yml`

**Interfaces:**
- Consumes: `needs.change-scope.outputs.package_publish_flow_changed`
- Produces: four steps with the exact condition `${{ needs.change-scope.outputs.package_publish_flow_changed == 'true' }}`

- [x] **Step 1: Write the failing workflow contract assertions**

Require the host-sealing, lifecycle checkout, JavaScript install, and publisher
validation steps to have the exact publisher-flow condition. Add mutations
that remove or change the condition and require rejection.

- [x] **Step 2: Run the focused contract to verify RED**

Run:

```bash
scripts/dev-shell.sh ruby scripts/test-homebrew-publisher-lifecycle-source.rb
```

Expected: failure because the four steps are currently unconditional.

- [x] **Step 3: Add the minimal workflow conditions**

Set the same `if` expression on exactly the four publisher-only steps. Preserve
their existing command bodies, source pin, and ordering. Update the structural
trust checker so reusable publication jobs still require unconditional host
sealing while staging preflight requires this exact condition.

- [x] **Step 4: Run focused verification to GREEN**

Run:

```bash
scripts/dev-shell.sh ruby scripts/test-homebrew-publisher-lifecycle-source.rb
scripts/dev-shell.sh ruby scripts/check-homebrew-publish-workflow-trust.rb
scripts/dev-shell.sh bash .github/scripts/test-homebrew-main-shell-change-scope.sh
```

Expected: all commands exit zero.

- [x] **Step 5: Verify workflow syntax and scoped diff**

Run:

```bash
scripts/dev-shell.sh actionlint .github/workflows/staging-build.yml
git diff --check
git diff --name-only origin/main...HEAD
```

Expected: actionlint and diff checks exit zero; only the two docs, workflow,
structural trust checker, and lifecycle-source contract test differ.

- [x] **Step 6: Commit**

```bash
git add \
.github/workflows/staging-build.yml \
scripts/check-homebrew-publish-workflow-trust.rb \
scripts/test-homebrew-publisher-lifecycle-source.rb \
docs/superpowers/specs/2026-08-16-scope-homebrew-publisher-suite-design.md \
docs/superpowers/plans/2026-08-16-scope-homebrew-publisher-suite.md
git commit -m "[CI] Reserve publisher integration for policy changes"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Scope the Homebrew Publisher Suite

## Problem

The `staging-build.yml` preflight runs
`scripts/test-homebrew-publish-workflow.sh` for every ordinary package or
runtime pull request routed through the legacy preflight. The suite exercises
the complete privileged Homebrew publication lifecycle and takes about
17 minutes. Most candidate iterations do not change that lifecycle, so the
cost delays feedback without increasing evidence for the changed code.

## Decision

Run the complete publisher suite in staging only when the protected change
scope reports `package_publish_flow_changed == true`. This includes both
publisher-only changes and mixed changes that touch publisher policy.

Apply the same condition to the four publisher-only setup steps:

- seal conventional host runtime ownership;
- check out the pinned Homebrew lifecycle source;
- install JavaScript dependencies for the publisher preflight; and
- run the complete publisher trust suite.

Ordinary package, runtime, and bottle-candidate iterations retain the normal
preflight matrix, xtask build, and relevant tests. The reusable publisher
workflows keep their existing immutable workflow and implementation digests;
this change does not weaken or regenerate those publication identities.

## Boundaries

- Do not change Formula source, package manifests, bottle contracts, ABI
identity, request identity, or candidate selection.
- Do not alter `scripts/test-homebrew-publish-workflow.sh`; it remains the
complete publisher-policy regression suite.
- Do not add a second path classifier. Use the existing fail-closed
`package_publish_flow_changed` output.
- Keep the pinned Homebrew checkout and explicit source environment whenever
the suite runs.

## Validation

The workflow contract test must reject an unconditional publisher setup or
suite step, reject a different condition, and retain the exact pinned Homebrew
source and explicit environment. The existing workflow trust checker and
change-scope contracts must remain green. A hosted ordinary package PR will be
used later to measure the avoided wall time; local structural tests do not
make a hosted timing claim.
12 changes: 11 additions & 1 deletion scripts/check-homebrew-publish-workflow-trust.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
REPO_ROOT, "scripts/prepare-homebrew-recipe-host-runtime.py"
)
HOST_RUNTIME_PREPARATION_STEP = "Seal conventional host runtime ownership"
PUBLISHER_FLOW_CONDITION =
"${{ needs.change-scope.outputs.package_publish_flow_changed == 'true' }}"
STAGING_PREFLIGHT_LABEL = ".github/workflows/staging-build.yml:preflight"
PRIVILEGED_RECIPE_ENTRYPOINTS = %w[
scripts/homebrew-bottle-build.sh
scripts/homebrew-verify-poured-bottle.sh
Expand Down Expand Up @@ -201,11 +204,18 @@ def check_privileged_recipe_host_runtime(workflows)
)
actual.each do |label, (steps, privileged_indices)|
expected_command = PRIVILEGED_RECIPE_JOBS.fetch(label)
expected_condition =
label == STAGING_PREFLIGHT_LABEL ? PUBLISHER_FLOW_CONDITION : nil
preparation_indices = steps.each_index.select do |index|
step = steps.fetch(index)
condition_matches = if expected_condition
step["if"] == expected_condition
else
!step.key?("if")
end
step["name"] == HOST_RUNTIME_PREPARATION_STEP &&
step["run"] == expected_command &&
!step.key?("if") &&
condition_matches &&
!step.key?("continue-on-error")
end
check(
Expand Down
27 changes: 26 additions & 1 deletion scripts/test-homebrew-publisher-lifecycle-source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"
SOURCE_STEP = "Checkout exact Homebrew lifecycle source"
VALIDATION_STEP = "Validate Homebrew publisher trust contract"
HOST_RUNTIME_STEP = "Seal conventional host runtime ownership"
JAVASCRIPT_STEP = "Install JavaScript dependencies for Homebrew preflight"
PUBLISHER_FLOW_CONDITION =
"${{ needs.change-scope.outputs.package_publish_flow_changed == 'true' }}"

def check(condition, message)
raise message unless condition
Expand Down Expand Up @@ -46,12 +50,20 @@ def check_contract(workflow, lifecycle, roots, dev_shell)
"statuses" => "write",
}, "staging preflight authority changed")
steps = preflight.fetch("steps")
host_runtime = named_step(steps, HOST_RUNTIME_STEP)
source = named_step(steps, SOURCE_STEP)
javascript = named_step(steps, JAVASCRIPT_STEP)
validation = named_step(steps, VALIDATION_STEP)
commit = lifecycle_commit(lifecycle)

[host_runtime, source, javascript, validation].each do |step|
check(step.fetch("if") == PUBLISHER_FLOW_CONDITION,
"#{step.fetch("name")} is not scoped to publisher-flow changes")
end

check(source == {
"name" => SOURCE_STEP,
"if" => PUBLISHER_FLOW_CONDITION,
"uses" => CHECKOUT_ACTION,
"with" => {
"persist-credentials" => false,
Expand All @@ -62,7 +74,7 @@ def check_contract(workflow, lifecycle, roots, dev_shell)
}, "staging lifecycle source is not the exact read-only checkout")
check(steps.index(source) < steps.index(validation),
"staging validates the publisher before provisioning its source")
check(validation.keys.sort == %w[name run],
check(validation.keys.sort == %w[if name run],
"staging lifecycle validation gained ambient configuration")

expected_run = <<~SHELL
Expand Down Expand Up @@ -109,6 +121,19 @@ def expect_rejection(label)
check_contract(workflow, lifecycle, roots, dev_shell)

{
"unconditional publisher setup" => lambda { |candidate|
steps = candidate.fetch("jobs").fetch("preflight").fetch("steps")
named_step(steps, HOST_RUNTIME_STEP).delete("if")
},
"differently scoped publisher setup" => lambda { |candidate|
steps = candidate.fetch("jobs").fetch("preflight").fetch("steps")
named_step(steps, JAVASCRIPT_STEP)["if"] =
"${{ needs.change-scope.outputs.homebrew_publisher_only_changed == 'true' }}"
},
"unconditional publisher validation" => lambda { |candidate|
steps = candidate.fetch("jobs").fetch("preflight").fetch("steps")
named_step(steps, VALIDATION_STEP).delete("if")
},
"mutable Homebrew source" => lambda { |candidate|
named_step(
candidate.fetch("jobs").fetch("preflight").fetch("steps"),
Expand Down
Loading