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
30 changes: 30 additions & 0 deletions .github/actions/rewrite-require/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright Strata Contributors
# SPDX-License-Identifier: Apache-2.0 OR MIT
name: Rewrite require to local path
description: >
Rewrite a [[require]] block in a downstream lakefile.toml to point at a
local path instead of a git/rev dependency. Used by downstream-check
workflows to build a downstream repo against an upstream PR's checked-out
code (fork-safe: the PR head SHA need not be fetchable from the upstream
remote). The targeted block keeps its `name`; any git/rev/path/scope lines
are replaced with a single `path` line. Fails if no matching block exists.

inputs:
lakefile:
description: Path to the downstream lakefile.toml to rewrite.
required: true
package:
description: The require `name` to override (e.g. "Strata", "StrataDDM").
required: true
path:
description: Local path to substitute (relative to the lakefile's repo root).
required: true

runs:
using: composite
steps:
- name: Rewrite require
shell: bash
run: |
python3 "${{ github.action_path }}/../../scripts/rewrite_require.py" \
"${{ inputs.lakefile }}" "${{ inputs.package }}" "${{ inputs.path }}"
114 changes: 114 additions & 0 deletions .github/scripts/rewrite_require.py

@robin-aws robin-aws Jun 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely don't love this approach since a) it's potentially-brittle grepping through config files, and b) it would need to be expanded to support lakefile.lean files as well.

Here are two alternatives (which I should have thought of earlier!)

  1. Generate a package overrides file to point references to the package to the local path.
  2. Create a local reservoir-style image of the modified package as if it was deployed there, and set the RESERVOIR_API_URL environment variable to file:///...

1 is probably the way to go. I mention 2 for completeness (and because I think we can use it for other purposes).

Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Copyright Strata Contributors
# SPDX-License-Identifier: Apache-2.0 OR MIT
"""Rewrite a `[[require]]` block in a lakefile.toml to point at a local path.

Used by the downstream-check workflow: a downstream repo's git/rev dependency
on an upstream package (e.g. Strata) is replaced with a local `path` require so
the build runs against the PR's checked-out code instead of a published rev.
This is fork-safe (no need for the PR head SHA to be fetchable from the upstream
remote) and faster (no re-clone of the upstream).

Usage:
python rewrite_require.py <lakefile.toml> <package-name> <local-path>

Example:
python rewrite_require.py downstream/lakefile.toml Strata ../upstream

The targeted `[[require]]` block keeps its `name` line; any `git`, `rev`,
`path`, `subDir`, or `scope` lines in that block are dropped and replaced with a
single `path = "<local-path>"` line. Other require blocks are left untouched.
Exits non-zero if no matching block is found, so the workflow fails loudly
rather than silently building against the wrong rev.
"""

import sys

# Keys that describe a dependency source. We strip all of them from the target
# block and substitute a single `path` line, so a prior `git`+`rev` pair can't
# linger and shadow the local override.
SOURCE_KEYS = ("git", "rev", "path", "subdir", "scope", "url")


def main() -> int:
if len(sys.argv) != 4:
print(__doc__)
return 2
lakefile, pkg, local_path = sys.argv[1], sys.argv[2], sys.argv[3]

with open(lakefile, encoding="utf-8") as f:
lines = f.readlines()

out: list[str] = []
i = 0
n = len(lines)
rewrote = False

while i < n:
line = lines[i]
if line.strip() == "[[require]]":
# Collect the block: every line up to (but not including) the next
# table header (`[` at column 0) or EOF.
block = [line]
j = i + 1
while j < n and not lines[j].lstrip().startswith("["):
block.append(lines[j])
j += 1

# Does this block require the package we're overriding?
is_target = any(
_is_name(bl, pkg) for bl in block
)
if is_target:
rewrote = True
out.append("[[require]]\n")
# Preserve the name line verbatim; drop source keys; keep
# anything else (e.g. options) as-is. Trailing blank lines are
# collected separately so the substituted `path` line sits
# directly under the kept keys, not after a gap.
kept: list[str] = []
trailing_blanks: list[str] = []
for bl in block[1:]:
if bl.strip() == "":
trailing_blanks.append(bl)
continue
# A non-blank line ends any run of pending blanks (they
# were interior, not trailing) — flush them back.
kept.extend(trailing_blanks)
trailing_blanks = []
key = bl.split("=", 1)[0].strip().lower()
if key in SOURCE_KEYS:
continue
kept.append(bl)
out.extend(kept)
out.append(f'path = "{local_path}"\n')
out.extend(trailing_blanks)
else:
out.extend(block)
i = j
else:
out.append(line)
i += 1

if not rewrote:
print(
f"ERROR: no [[require]] block with name = \"{pkg}\" found in {lakefile}",
file=sys.stderr,
)
return 1

with open(lakefile, "w", encoding="utf-8") as f:
f.writelines(out)
print(f"Rewrote require \"{pkg}\" -> path = \"{local_path}\" in {lakefile}")
return 0


def _is_name(line: str, pkg: str) -> bool:
s = line.strip()
if not s.startswith("name"):
return False
_, _, rhs = s.partition("=")
return rhs.strip().strip('"').strip("'") == pkg


if __name__ == "__main__":
raise SystemExit(main())
134 changes: 134 additions & 0 deletions .github/workflows/downstream-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Copyright Strata Contributors
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
# Downstream check: builds each repo that depends on Strata against this PR's
# code, so we catch breakage before it lands on main. Advisory only — these
# checks are visible on the PR but do not gate merge.
#
# Mechanism: check out the PR's Strata, clone each downstream, rewrite the
# downstream's `require "Strata"` to a local path pointing at the checked-out
# PR code (fork-safe: no need for the PR head SHA to exist on the strata-org
# remote), then `lake update Strata` + build + test.
#
# Trigger: non-draft PRs only (ready_for_review + every push via synchronize).
# We deliberately do NOT support an issue_comment (`!downstream-check`) trigger:
# that runs in the privileged default-branch context, and building untrusted PR
# code there is a cache-poisoning / code-execution vector (CodeQL
# actions/cache-poisoning/poisonable-step). pull_request runs the same build in
# an isolated, unprivileged context, which is safe. If on-demand checks are
# wanted later, add them via label-indirection (a privileged workflow turns a
# comment into a label; `pull_request: types: [labeled]` does the build
# unprivileged) — never by reintroducing issue_comment here.

name: Downstream check

on:
pull_request:
types: [ready_for_review, synchronize]

# One in-flight run per PR; a new push cancels the previous downstream check.
concurrency:
group: downstream-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read

jobs:
downstream:
# Non-draft PRs only. (The `types` filter already restricts to
# ready_for_review/synchronize.)
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
strategy:
# Don't let one broken downstream hide the status of the others.
fail-fast: false
matrix:
include:
# Boole and Python define a lake testDriver, so `test: true` builds
# and runs their suites.
- repo: Strata-Boole
test: true
- repo: Strata-Python
test: true
# Strata-CLI has no testDriver; mirror its own CI — build, then
# exercise the binary + examples via post_build (run below).
- repo: Strata-CLI
test: false
post_build: |
lake exe strata --help
./scripts/run_examples.sh
# The job name is what reviewers see in the PR's Checks tab:
# "Downstream / Strata-Boole", etc.
name: ${{ matrix.repo }}
steps:
- name: Check out PR's Strata
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
path: upstream

- name: Clone downstream (${{ matrix.repo }})
run: git clone --depth 1 https://github.com/strata-org/${{ matrix.repo }}.git downstream

- name: Override Strata require -> local PR checkout
uses: ./upstream/.github/actions/rewrite-require
with:
lakefile: downstream/lakefile.toml
package: Strata
path: ../upstream

- name: Install cvc5
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
uses: ./upstream/.github/actions/install-cvc5
- name: Install z3
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
uses: ./upstream/.github/actions/install-z3

# Install the Lean toolchain (elan/lake) before `lake update`. elan is
# installed unconditionally by lean-action; auto-config: false skips its
# build/test so this step only provisions the toolchain. The build itself
# runs in the "Build downstream" step below.
- name: Set up Lean toolchain
uses: leanprover/lean-action@v1
with:
lake-package-directory: downstream
auto-config: false
use-github-cache: false

# Cache keyed on upstream head SHA + downstream repo. The restore-keys
# fallback lets a new push reuse the previous build for this PR; lake
# rebuilds the modules whose (overridden, local) Strata source changed.
# Trades a small stale-result risk for ~15-30 min, matching Strata CI's
# PR caching posture. This is advisory CI, so the trade is acceptable.
- name: Restore lake cache
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
uses: actions/cache/restore@v5
with:
path: |
downstream/.lake
key: downstream-${{ matrix.repo }}-${{ runner.os }}-${{ github.event.pull_request.head.sha }}
restore-keys: |
downstream-${{ matrix.repo }}-${{ runner.os }}-

- name: lake update Strata
working-directory: downstream
run: lake update Strata

- name: Build downstream
uses: leanprover/lean-action@v1
with:
lake-package-directory: downstream
use-github-cache: false
test: ${{ matrix.test }}

# Repos without a lake testDriver verify via their own post-build steps.
- name: Post-build checks (${{ matrix.repo }})
if: matrix.post_build != ''
working-directory: downstream
run: ${{ matrix.post_build }}

- name: Save lake cache
if: always()
uses: actions/cache/save@v5
with:
path: |
downstream/.lake
key: downstream-${{ matrix.repo }}-${{ runner.os }}-${{ github.event.pull_request.head.sha }}
Loading