Skip to content
Draft
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
16 changes: 13 additions & 3 deletions .github/workflows/ci_nightly.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
---
# Runs on a nightly schedule (and optionally via manual dispatch).
#
# Executes the full CI matrix: stability (latest tags) and frontier
# (latest branches). Both runs file GitHub issues with their results
# unless manually suppressed via workflow_dispatch inputs.
# Executes the full CI matrix: stability (latest tags), frontier
# (latest branches), and manifest-declared mixed profiles. Runs file GitHub
# issues with their results unless manually suppressed via workflow_dispatch
# inputs.

name: CI (Nightly)

Expand Down Expand Up @@ -35,6 +36,15 @@ jobs:
- name: frontier
issue_label: scenarios-run-frontier
issue_title: "FOC Devnet scenarios run report (frontier)"
- name: stability-frontier-lotus
issue_label: scenarios-run-stability-frontier-lotus
issue_title: "FOC Devnet scenarios run report (stability-frontier-lotus)"
- name: stability-frontier-curio
issue_label: scenarios-run-stability-frontier-curio
issue_title: "FOC Devnet scenarios run report (stability-frontier-curio)"
- name: stability-frontier-filecoin-services
issue_label: scenarios-run-stability-frontier-filecoin-services
issue_title: "FOC Devnet scenarios run report (stability-frontier-filecoin-services)"
uses: ./.github/workflows/ci_run.yml
with:
name: ${{ matrix.name }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci_run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Reusable workflow: a single foc-devnet CI run.
#
# Called by ci_pull_request.yml (default config, no reporting) and
# ci_nightly.yml (stability / frontier matrix, issue reporting enabled).
# ci_nightly.yml (manifest-declared nightly profiles, issue reporting enabled).
#
# The dependency profile controls the versions of compatibility-sensitive
# server and client components used by the run.
Expand All @@ -13,11 +13,11 @@ on:
workflow_call:
inputs:
name:
description: 'Human-readable run name (e.g. default, stability, frontier)'
description: 'Human-readable run name (e.g. default, stability, frontier, stability-frontier-curio)'
required: true
type: string
profile:
description: 'Dependency profile: default, stability, or frontier'
description: 'Dependency profile declared in ci/dependency-profiles.json'
required: true
type: string
enable_reporting:
Expand Down
10 changes: 6 additions & 4 deletions README_ADVANCED.md
Original file line number Diff line number Diff line change
Expand Up @@ -1324,7 +1324,9 @@ Scenarios run automatically in CI after the devnet starts. On nightly runs (or m
CI resolves compatibility-sensitive dependencies from `ci/dependency-profiles.json`.
Pull requests use the pinned `default` profile, while nightly `stability` runs use
the latest final releases and nightly `frontier` runs pin current development
branch heads to immutable commits. The resolved metadata path is exposed to
scenarios as `CI_DEPENDENCY_METADATA`; Synapse SDK and filecoin-pin also receive
their exact source, version/ref, and commit through `SYNAPSE_SDK_*` and
`FILECOIN_PIN_*` environment variables.
branch heads to immutable commits. Nightly CI also runs manifest-declared mixed
profiles such as `stability-frontier-curio`, where all dependencies come from
`stability` except the named component, which comes from `frontier`. The resolved
metadata path is exposed to scenarios as `CI_DEPENDENCY_METADATA`; Synapse SDK
and filecoin-pin also receive their exact source, version/ref, and commit
through `SYNAPSE_SDK_*` and `FILECOIN_PIN_*` environment variables.
33 changes: 30 additions & 3 deletions ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,40 @@ Its resolver is located in `scripts/resolve-ci-dependencies.py`.

## Profiles

The manifest currently supports three profiles:
The manifest declares valid profiles in its top-level `profiles` object:

- `default`: used by PR CI, a known-working set of client versions.
- `stability`: used by nightly CI to test stable releases.
- `frontier`: used by nightly CI to test branch heads.
- `stability-frontier-lotus`: used by nightly CI to test stable releases
except Lotus, which is resolved from `frontier`.
- `stability-frontier-curio`: used by nightly CI to test stable releases
except Curio, which is resolved from `frontier`.
- `stability-frontier-filecoin-services`: used by nightly CI to test stable
releases except filecoin-services, which is resolved from `frontier`.

Each component must define a selection for each profile.
Each component must define a selection for every component profile referenced by
the top-level profile definitions. Today those component selections are
`default`, `stability`, and `frontier`.

Top-level profile definitions have a `base` component profile and can override
specific components:

```json
{
"stability-frontier-curio": {
"base": "stability",
"components": {
"curio": "frontier"
}
}
}
```

In that example, Curio resolves from its `frontier` selection while every other
component resolves from `stability`. A profile is valid only if it is explicitly
declared in `profiles`; for example, `stability-frontier-filecoin-pin` does not
exist unless added there.

## Component Fields

Expand All @@ -20,7 +47,7 @@ Top-level component fields:
- `repository`: Git repository URL.
- `npm_package`: npm package name, for components that are resolved through npm
metadata.
- `default`, `stability`, `frontier`: profile selections.
- `default`, `stability`, `frontier`: component profile selections.

Profile selections always have a `strategy`. Some strategies require additional
fields.
Expand Down
31 changes: 30 additions & 1 deletion ci/dependency-profiles.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
{
"schema_version": 1,
"schema_version": 2,
"profiles": {
"default": {
"base": "default"
},
"stability": {
"base": "stability"
},
"frontier": {
"base": "frontier"
},
"stability-frontier-lotus": {
"base": "stability",
"components": {
"lotus": "frontier"
}
},
"stability-frontier-curio": {
"base": "stability",
"components": {
"curio": "frontier"
}
},
"stability-frontier-filecoin-services": {
"base": "stability",
"components": {
"filecoin-services": "frontier"
}
}
},
"components": {
"lotus": {
"repository": "https://github.com/filecoin-project/lotus.git",
Expand Down
70 changes: 64 additions & 6 deletions scripts/resolve-ci-dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import subprocess
from pathlib import Path

PROFILES = {"default", "stability", "frontier"}
MANIFEST_SCHEMA_VERSION = 2
INIT_COMPONENT_FLAGS = {
"lotus": "--lotus",
"curio": "--curio",
Expand Down Expand Up @@ -44,19 +44,77 @@ def load_manifest(path: Path) -> dict:
f"Cannot load dependency manifest {path}: {error}"
) from error

if manifest.get("schema_version") != 1:
raise ResolutionError("Dependency manifest schema_version must be 1")
if manifest.get("schema_version") != MANIFEST_SCHEMA_VERSION:
raise ResolutionError(
f"Dependency manifest schema_version must be {MANIFEST_SCHEMA_VERSION}"
)
components = manifest.get("components")
if not isinstance(components, dict):
raise ResolutionError("Dependency manifest must contain a components object")
profiles = manifest.get("profiles")
if not isinstance(profiles, dict):
raise ResolutionError("Dependency manifest must contain a profiles object")

required = set(INIT_COMPONENT_FLAGS) | {"synapse-sdk", "filecoin-pin"}
missing = sorted(required - set(components))
if missing:
raise ResolutionError(f"Dependency manifest is missing: {', '.join(missing)}")
validate_profiles(profiles, components)
return manifest


def validate_profiles(profiles: dict, components: dict) -> None:
for profile_name, profile in profiles.items():
if not isinstance(profile_name, str) or not profile_name:
raise ResolutionError("Profile names must be non-empty strings")
if not isinstance(profile, dict):
raise ResolutionError(f"Profile {profile_name!r} must be an object")

base = profile.get("base")
if not isinstance(base, str) or not base:
raise ResolutionError(f"Profile {profile_name!r} base must be a string")

component_overrides = profile.get("components", {})
if not isinstance(component_overrides, dict):
raise ResolutionError(
f"Profile {profile_name!r} components must be an object"
)

unknown_components = sorted(set(component_overrides) - set(components))
if unknown_components:
raise ResolutionError(
f"Profile {profile_name!r} references unknown components: "
f"{', '.join(unknown_components)}"
)

for component_name, component in components.items():
selection_profile = component_overrides.get(component_name, base)
if not isinstance(selection_profile, str) or not selection_profile:
raise ResolutionError(
f"Profile {profile_name!r} selection for {component_name!r} "
"must be a string"
)
if selection_profile not in component:
raise ResolutionError(
f"Profile {profile_name!r} selects {selection_profile!r} for "
f"{component_name}, but that component has no such selection"
)


def component_profile_map(manifest: dict, profile_name: str) -> dict[str, str]:
profiles = manifest["profiles"]
if profile_name not in profiles:
raise ResolutionError(f"Unknown profile {profile_name!r}")

profile = profiles[profile_name]
base = profile["base"]
component_overrides = profile.get("components", {})
return {
component_name: component_overrides.get(component_name, base)
for component_name in manifest["components"]
}


def parse_ls_remote(output: str) -> list[tuple[str, str]]:
refs = []
for line in output.splitlines():
Expand Down Expand Up @@ -225,6 +283,7 @@ def resolve_component(
resolved = {
"name": name,
"repository": repository,
"selection_profile": profile,
"strategy": strategy,
}

Expand Down Expand Up @@ -310,11 +369,10 @@ def scenario_environment(metadata_path: Path, components: dict) -> dict:


def resolve(args) -> None:
if args.profile not in PROFILES:
raise ResolutionError(f"Unknown profile {args.profile!r}")
manifest = load_manifest(args.manifest)
component_profiles = component_profile_map(manifest, args.profile)
components = {
name: resolve_component(name, component, args.profile)
name: resolve_component(name, component, component_profiles[name])
for name, component in manifest["components"].items()
}
metadata = {
Expand Down
Loading