Skip to content

Many projects ​

A portability layer used by a string library, used by a parser, used by your application: each in its own repository, with its own tags, CI and boilerplate, and every change at the bottom rippling upwards. fobis ecosystem manages such a family of FoBiS projects as a whole: it shows the state of every repository, runs commands across them in dependency order, builds the dependents against an unreleased change, and releases them as a train. fobis scaffold keeps their CI workflows, scripts and documentation configuration identical. The outputs below come from a private three-project demo you can build in a minute: see Try it offline.

How it works:

  1. The registry. The ecosystem is an explicit list of local repositories in your user configuration, [ecosystem] root and projects. A project is a git repository with a fobos file; its name is its directory name. Nothing is managed until you register it.
  2. The graph. The edges come from each project's own fobos [dependencies], the section fobis fetch reads: a dependency is an ecosystem dependency when its key or the repository name of its URL matches a registered project (case-insensitively), or when it is a path= dependency whose directory is one. Any other dependency is external: listed, not managed.
  3. The release order. Projects are grouped in levels, each one level above its deepest dependency. Every acting command walks the projects in this order, dependencies first.
  4. The release rule. A project needs a release when a commit since its latest tag changes a file outside docs/* .github/* *.md scripts/*; the Conventional Commits types of those commits give the bump.
  5. Nothing written without consent. discover, graph, dashboard and impact only read (the dashboard runs git fetch, which updates remote-tracking refs only); check builds copies. Only release commits and pushes, after you approve the plan, and a confirmation that cannot be answered (no terminal) counts as no.

Tutorial: the ecosystem track. Reference: ecosystem, scaffold.

fobis ecosystem graph and dashboard

Register the projects ​

The registry lives in ~/.config/fobis/config.ini ($XDG_CONFIG_HOME/fobis/config.ini when set; --config FILE on any subcommand reads another):

ini
[ecosystem]
; the directory the projects live in
root     = ~/fortran
; names under root, or paths; whitespace or newline separated
projects = PENF FACE BeFoR64 StringiFor
           FLAP

discover lists the git repositories with a fobos file directly under the root (no recursion) that are not registered yet, and prints the command that would register them. Pick the ones you want, not necessarily all:

console
$ fobis ecosystem discover
Unregistered FoBiS projects under ~/eco:
  app
  core
  mathx

Register the ones you want to manage (not necessarily all):
  fobis ecosystem add app core mathx
console
$ fobis ecosystem add core mathx app
Registered core, mathx, app in ~/eco/.config/fobis/config.ini
See where they sit: fobis ecosystem graph
bash
fobis ecosystem discover --root ~/work      # scan another directory
fobis ecosystem add FiNeR ~/work/MyLib      # names under root, or any path
fobis ecosystem remove FiNeR                # the repository itself is not touched
  • The name identifies the project. add accepts only git repositories with a fobos file whose directory name no registered project has; an already registered one is reported and skipped. A project directly under root is written as its bare name, any other as a ~/... or absolute path.
  • All or nothing. One invalid argument and the configuration is not touched (exit 1). remove notes the projects that still depend on the one removed: it becomes an external dependency for them.
  • Your comments survive. add and remove rewrite only the projects key (creating the section or the file when missing), atomically; long lists wrap onto continuation lines.
  • No inline comments in the configuration: FLAP ; the parser registers ;, the and parser as projects.
  • Two registered directories with the same name: the second is ignored, with a warning.

The graph and the release order ​

console
$ fobis ecosystem graph
Release order (dependencies first)
   1. core  (level 0)
   2. mathx  (level 1)
   3. app  (level 2)

Dependency tree
app
|-- mathx
|   `-- core
`-- core

Level 0 holds the projects with no ecosystem dependency; releasing level by level never releases a project before one of its dependencies. A subtree shown already is marked (*), and the external dependencies are listed last.

bash
fobis ecosystem graph --json      # projects (depends_on, dependents, external), levels, release_order, cycle
  • A dependency cycle is printed as dependency cycle, not ordered: A, B and its projects are left out of the release order; the acting commands still visit them, after all the others. graph exits 0 all the same.
  • Exit 1 when no project is registered, for every subcommand that reads the registry.

The dashboard ​

One row per project: git state, release state, CI, boilerplate drift, dependency pins.

console
$ fobis ecosystem dashboard --no-ci
Project  Branch  Tree   Upstream  Version / tag    Unreleased           CI       Drift  Pins
-------  ------  -----  --------  ---------------  -------------------  -------  -----  -----------------------
core     master  clean  in sync   v1.0.0 / v1.0.0  1 (minor -> v1.1.0)  unknown  0      -
mathx    master  clean  in sync   v1.0.0 / v1.0.0  1, none to release   unknown  0      core stale
app      master  clean  in sync   v1.0.0 / v1.0.0  0                    unknown  0      mathx stale, core stale
ColumnShowsLook at it when
Treeclean, or the number of changed and untracked filesnot clean
Upstreamin sync, +ahead/-behind, or no upstreamnot in sync
Version / tagthe VERSION file (else a literal fobos [project] version) against the latest reachable tagthey differ (highlighted)
Unreleasedcommits since the tag, how many are releasable, the bump and the next versionsomething to release
CIGitHub Actions at HEAD: passing, failing, running, none (no run), not pushed, unknownfailing
Driftscaffold-managed files not in sync, project-owned ones excluded (? when it cannot be computed)not 0
Pinsthe ecosystem dependencies not at their head, with their statestale, unlocked, diverged

The Unreleased cell reads 0; N (minor -> v1.1.0) when every commit is releasable; N, R to release (...); or N, none to release when only docs or CI changed. CI is the latest run of every workflow for the HEAD commit (a re-run supersedes the previous one): failing when any concluded otherwise than success, skipped or neutral, running while any is not completed.

A pin compares the commit each project locked in <deps_dir>/fobos.lock (written by fobis fetch) with the dependency's remote default-branch head. Ecosystem projects usually follow the default branch of their dependencies, so a fix at the bottom reaches the dependents without a cascade of version bumps; the pin says whether they are built against it:

PinMeaningFix
currentbuilt against the dependency's head
stalethe head has moved on since the last fetchfobis ecosystem fetch
aheadthe lock is newer than the head FoBiS knowsgit fetch the dependency (drop --no-fetch)
divergedlock and head on unrelated historiesinspect the dependency
unlockedthe dependency was never fetchedfobis fetch in the project
unknownthe dependency has no remote headpush the dependency
NAME at DIRa path= dependency, used as it is in its directory
bash
fobis ecosystem dashboard                # git fetch in every project first, then the table
fobis ecosystem dashboard --no-fetch     # offline: upstream, CI and pins as of the last fetch
fobis ecosystem dashboard --no-ci        # do not ask GitHub (CI reads unknown)
fobis ecosystem dashboard -j 16          # projects examined at once (default 8)
  • The fetch is what makes it right: ahead/behind, CI at HEAD and the dependency heads are only current after it.
  • CI reads unknown everywhere when gh is missing or not authenticated (gh auth status), or origin is not on GitHub. Nothing fails.
  • A pin stays stale after a fetch when the commits are not pushed: pins compare with the dependency's remote head.
  • An unreadable project (no directory, not a git repository, no fobos, a broken fobos) shows its error across the row; the other projects are not affected. Problems met while reading a project (a failed fetch) are noted below the table.
  • Each project's state, most severe first, colours the HTML diagram: error, ci-failing, stale (a pin stale, diverged or unlocked), unreleased (releasable commits), ok.

The dashboard as JSON and as a web page ​

For scripts and agents, every field of every project, plus the levels, the release order and any cycle:

console
$ fobis ecosystem dashboard --no-ci --json | head -40
{
  "generated": "2026-10-14T07:40:20",
  "projects": [
    {
      "name": "core",
      "path": "~/eco/core",
      "slug": "",
      "branch": "master",
      "head": "c3b0b47c7baf7e9829850d56422b49edb28c04c7",
      "dirty": 0,
      "ahead": 0,
      "behind": 0,
      "fetched": true,
      "version": "v1.0.0",
      "tag": "v1.0.0",
      "unreleased": 1,
      "releasable": 1,
      "bump": "minor",
      "next_version": "v1.1.0",
      "pushed": true,
      "ci": "unknown",
      "drift": 0,
      "pins": [],
      "errors": [],
      "state": "unreleased"
    },
    {
      "name": "mathx",
      "path": "~/eco/mathx",
      "slug": "",
      "branch": "master",
      "head": "fae4cf37304e82904e1bb9f6a09fea5b2f46ff9f",
      "dirty": 0,
      "ahead": 0,
      "behind": 0,
      "fetched": true,
      "version": "v1.0.0",
      "tag": "v1.0.0",
      "unreleased": 1,
      "releasable": 0,

ahead/behind are null without an upstream, drift is null when unknown, bump is null and next_version empty when nothing is to release; each pin is {dep, locked, head, state, path}; errors holds the problems met while reading the project, failed fetches included. After the projects come levels, release_order, cycle and warnings.

bash
# the projects with something to release
fobis ecosystem dashboard --no-ci --json | jq -r '.projects[] | select(.releasable > 0) | .name'

A self-contained HTML page: the same table with links to each repository, its Actions and its releases, the release order, and the dependency diagram with every node coloured by its project's state.

console
$ fobis ecosystem dashboard --no-ci --format html --no-open -o dashboard.html && ls -s --block-size=K dashboard.html
Dashboard written to ~/eco/dashboard.html
16K dashboard.html

HTML dashboard, Catppuccin Mocha

HTML dashboard, GitHub Light

bash
fobis ecosystem dashboard --format html                       # ~/.cache/fobis/ecosystem.html, opened in the browser
fobis ecosystem dashboard --format html --theme nord --mode dark

The page has Theme and Mode selectors that switch live and are remembered by the browser. Nine palettes, each with a light and a dark variant: github (the default), solarized, dracula (Alucard / Dracula), nord (Snow Storm / Nord), tokyo-night, catppuccin (Latte / Mocha), gruvbox, one, rose-pine (Dawn / Rosé Pine). The mode is auto (follow the system, live), light or dark. Set your defaults in the user configuration:

ini
[ecosystem]
theme = catppuccin
mode  = auto
  • Contrast is enforced: secondary, link and status text is darkened or lightened, keeping its hue, to a 4.5:1 contrast with the background.
  • Offline: the diagram is drawn by Mermaid from a CDN; without network the page shows the table and the diagram source. Without JavaScript it falls back to the GitHub palette.
  • XDG_CACHE_HOME moves the default page; -o FILE writes elsewhere, --no-open does not start a browser.
  • An unknown theme or mode exits 2 (checked only when a page is written).
  • On WSL a Windows browser cannot open a file:///home/... path: write the page, then open it from Windows.
bash
fobis ecosystem dashboard --format html --no-open
explorer.exe "$(wslpath -w ~/.cache/fobis/ecosystem.html)"

Run a command in every project ​

exec runs a command in each project's directory through bash -c, in release order, its output streaming live under a ==> PROJECT: command header, then sums up:

console
$ fobis ecosystem exec -- git log --oneline -1

==> core: git log --oneline -1
c3b0b47 feat(core): add halve()

==> mathx: git log --oneline -1
fae4cf3 docs: explain quadruple()

==> app: git log --oneline -1
48ded4b feat(app): first release

Project  Result        Time  Detail
core     passed        0.0s
mathx    passed        0.0s
app      passed        0.0s

3 passed, 0 failed
console
$ fobis ecosystem exec -- fobis build --quiet

==> core: fobis build --quiet

Building src/test_core.f90
Compiling src/core_m.f90 serially
Compiling src/test_core.f90 serially
Linking build/test_core
Target src/test_core.f90 has been successfully built

==> mathx: fobis build --quiet

Building src/test_mathx.f90
Compiling third_party/core/src/core_m.f90 serially
Compiling src/mathx_m.f90 serially
Compiling src/test_mathx.f90 serially
Linking build/test_mathx
Target src/test_mathx.f90 has been successfully built

==> app: fobis build --quiet

Building src/app.f90
Compiling third_party/core/src/core_m.f90 serially
Compiling third_party/mathx/src/mathx_m.f90 serially
Compiling src/app.f90 serially
Linking build/app
Target src/app.f90 has been successfully built

Project  Result        Time  Detail
core     passed        0.2s
mathx    passed        0.2s
app      passed        0.2s

3 passed, 0 failed
bash
fobis ecosystem exec -- git status -s                          # several words form one command
fobis ecosystem exec -- 'fobis clean && fobis build'           # one argument is a shell command line
fobis ecosystem exec --only core,mathx --fail-fast -- fobis test
fobis ecosystem exec --from mathx -- fobis test                # resume after a failure
ResultMeaning
passedthe command succeeded
failednon-zero exit
crashedkilled by a signal (the detail names it, e.g. SIGSEGV)
aborteda release declined at its prompt (release only)
skippednot run: unreadable project, nothing to do, a dependency not fetched
not runafter a failure, with --fail-fast (and always in release)
  • fobis means this FoBiS. fobis and FoBiS.py in the command, and in the fobos rules it runs, resolve to the FoBiS running exec, not to whichever one the PATH finds first.
  • Exit codes: 1 when a project failed or crashed; 2 without a command or for an unknown project name. --only keeps the release order whatever order you list the names in.
  • Clean builds everywhere. FoBiS skips up-to-date objects, so an object built with other flags can survive a flag change: fobis ecosystem exec -- 'fobis clean && fobis build' rules that out.

Bring every dependency to its head ​

fetch runs fobis fetch --update in every project with dependencies, dependencies first, so each lock ends at the current heads and the Pins column turns current:

console
$ fobis ecosystem fetch

==> mathx: fobis fetch --update
Updating core
Saved deps config to third_party/.deps_config.ini
Saved lock file to third_party/fobos.lock

==> app: fobis fetch --update
Updating mathx
Updating core
Saved deps config to third_party/.deps_config.ini
Saved lock file to third_party/fobos.lock

Project  Result        Time  Detail
core     skipped             no dependencies
mathx    passed        0.2s
app      passed        0.2s

2 passed, 0 failed

It takes --only, --from and --fail-fast like exec; a project with no dependency is skipped. More on [dependencies] and the lock file in Dependencies.

Check the dependents before releasing ​

The question before releasing a library is not whether it passes its own tests (its CI answers that) but whether it breaks the projects that use it. check answers it with your local working copy, uncommitted changes included, without touching any repository:

console
$ fobis ecosystem check core
Checking mathx, app against the local working copy of core

==> mathx: (fobis build --quiet) && (./build/test_mathx)

Building src/test_mathx.f90
Compiling third_party/core/src/core_m.f90 serially
Compiling src/mathx_m.f90 serially
Compiling src/test_mathx.f90 serially
Linking build/test_mathx
Target src/test_mathx.f90 has been successfully built
mathx: all tests passed

==> app: (fobis build --quiet) && (./build/app)

Building src/app.f90
Compiling third_party/core/src/core_m.f90 serially
Compiling third_party/mathx/src/mathx_m.f90 serially
Compiling src/app.f90 serially
Linking build/app
Target src/app.f90 has been successfully built
twice(3) =  6.0   quadruple(3) = 12.0

Project  Result        Time  Detail
mathx    passed        0.2s
app      passed        0.3s

2 passed, 0 failed

fobis ecosystem check

For every project depending on the checked one, directly or transitively, in release order:

  1. its working copy (tracked and untracked files; ignored files such as build products and fetched dependencies excluded) is copied to a temporary directory;
  2. its fetched dependencies are copied into the copy's deps_dir as it fetched them, with .deps_config.ini and fobos.lock, except the checked project, taken from its working copy; a path= dependency is copied to the same relative place;
  3. its check commands run there, chained with &&, from scratch: no stale object leaks into the result.

The commands come from the dependent's fobos: [ecosystem] check, one command per line; else fobis rule --ex makecoverage when the fobos has a [rule-makecoverage] (what the CI runs); else fobis build.

ini
[ecosystem]
check = fobis build --mode tests-gnu
        ./scripts/run_tests.sh

A change that breaks a dependent, twice renamed double in core and not committed:

console
$ fobis ecosystem check core
Checking mathx, app against the local working copy of core

==> mathx: (fobis build --quiet) && (./build/test_mathx)

Building src/test_mathx.f90
Compiling third_party/core/src/core_m.f90 serially
Compiling src/mathx_m.f90 serially
src/mathx_m.f90:2:19:

    2 |   use core_m, only: twice
      |                   1
Error: Symbol 'twice' referenced at (1) not found in module 'core_m'
src/mathx_m.f90:10:14:

   10 |     y = twice(twice(x))
      |              1
Error: Function 'twice' at (1) has no IMPLICIT type
src/mathx_m.f90:10:14:

   10 |     y = twice(twice(x))
      |              1
Error: Reference to impure function 'twice' at (1) within a PURE procedure


==> app: (fobis build --quiet) && (./build/app)

Building src/app.f90
Compiling third_party/core/src/core_m.f90 serially
Compiling third_party/mathx/src/mathx_m.f90 serially
third_party/mathx/src/mathx_m.f90:2:19:

    2 |   use core_m, only: twice
      |                   1
Error: Symbol 'twice' referenced at (1) not found in module 'core_m'
third_party/mathx/src/mathx_m.f90:10:14:

   10 |     y = twice(twice(x))
      |              1
Error: Function 'twice' at (1) has no IMPLICIT type
third_party/mathx/src/mathx_m.f90:10:14:

   10 |     y = twice(twice(x))
      |              1
Error: Reference to impure function 'twice' at (1) within a PURE procedure


Staged copies kept in ~/tmp/fobis-check-core-lsrgeosw

Project  Result        Time  Detail
mathx    failed        0.2s  exit 1
app      failed        0.2s  exit 1

0 passed, 2 failed: mathx, app
[exit status 1]
bash
fobis ecosystem check core --keep         # keep the staged copies even when everything passes
fobis ecosystem check core --fail-fast    # stop at the first failing dependent
  • The staged copies are removed after a full pass and kept, their path printed, when a check does not pass.
  • skipped: dependency X not fetched: the dependent never ran fobis fetch; run fobis ecosystem fetch.
  • Exit 1 unless every dependent passed, skipped ones included; 2 for an unknown project. A project nothing depends on has nothing to check (exit 0).

What an API change breaks ​

check builds the dependents; impact reads them. It compares the public interface of the project's Fortran modules between its latest tag (else HEAD) and its working tree, then finds every place in the dependents that uses what changed, without building anything:

console
$ git -C core diff --stat
 src/core_m.f90    | 6 +++---
 src/test_core.f90 | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)
console
$ fobis ecosystem impact core
core: public API changes since v1.0.0
           added   procedure core_m::double (src/core_m.f90:6)
           added   procedure core_m::halve (src/core_m.f90:12)
  BREAKING removed procedure core_m::twice (src/core_m.f90:6)

mathx: affected
  src/mathx_m.f90:2: use core_m, only: twice  [changed: twice]
    src/mathx_m.f90:10: twice: y = twice(twice(x))
app: affected
  src/app.f90:2: use core_m, only: twice  [changed: twice]
    src/app.f90:5: twice: print '(A,F5.1,A,F5.1)', "twice(3) =", twice(3.0), " quadruple(3) =", quadruple(3.0)

1 breaking change(s); 2 of 2 dependent project(s) affected.
[exit status 1]
bash
fobis ecosystem impact core                       # since the latest tag
fobis ecosystem impact core --base v1.0.0         # since another revision
fobis ecosystem impact core --format markdown     # for a release note or an issue
fobis ecosystem impact core --format json
  • The public interface of a module: its public procedures with their signature (prefixes, dummy arguments and their declarations, function result), generic interfaces and their specifics, derived types and their components, variables, and named constants with their value (a kind constant that changes is an interface change). Default access and access lists (public :: str, strz) are honoured.
  • Breaking is a removed or changed entity, or a removed module; an added one is listed, not breaking. When nothing breaks, the dependents are not scanned.
  • Re-exports: a facade module that uses a changed module and makes the changed name public changes too, transitively (PENF's penf re-exports penf_global_parameters_variables, and dependents only use penf).
  • A dependent is affected when its code references a changed name (only lists and renames resolved), when an only list imports a removed name, or when it uses a removed module. A plain use that never references what changed is not affected.
  • Only the dependent's own files are read (git ls-files, tracked and untracked): fetched dependencies and the contents of git submodules (vendored copies) are not its code.
  • Exit 1 when a dependent is affected; 2 for an unknown project or format.
  • --deep adds a model summary for the release train (what breaks, what to do), cited on these facts; --deep-context prints its input and runs no model. See AI assistance.

Shared boilerplate in one project ​

fobis scaffold keeps the boilerplate of a project identical to the templates bundled in FoBiS, with the project's name and URLs filled in: no template repository, no extra tool.

console
$ cd mathx && fobis scaffold status
  OUTDATED .github/workflows/ci.yml
  OK       .github/workflows/docs.yml
  OK       .github/workflows/install.yml
  OK       .github/actions/setup-build-env/action.yml
  OK       .github/actions/run-coverage-analysis/action.yml
  SKIPPED  scripts/release.sh (project-owned: fobos [scaffold] skip)
  OK       scripts/run_tests.sh
  OK       scripts/install.sh
  OK       scripts/compute-coverage.sh
  OK       docs/package.json
  OK       cliff.toml
  OK       docs/guide/contributing.md
  OK       CONTRIBUTING.md
  OK       LICENSE.gpl3.md
  OK       .github/workflows/release.yml
  OK       docs/.vitepress/config.mts
  OK       docs/ford.md
  OK       fpm.toml
  OK       fobos
bash
fobis scaffold status                     # every managed file and its state
fobis scaffold status --files "scripts/*" # a glob of managed paths (* also matches /)
fobis scaffold status --strict            # exit 1 on any drift: a CI check
fobis scaffold sync                       # each difference as a diff, then [Y/n] per file
fobis scaffold sync --dry-run             # the diffs, nothing written
fobis scaffold sync --files cliff.toml --yes
fobis scaffold init                       # create every missing managed file
fobis scaffold list                       # the managed files by category
console
$ cd mathx && fobis scaffold sync --yes && git status --short

--- .github/workflows/ci.yml ---
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -27,4 +27,3 @@
     - name: Run tests with coverage
       id: cov
       uses: ./.github/actions/run-coverage-analysis
-# local edit

  Written  .github/workflows/ci.yml

1 file(s) updated.
StateMeaning
OKidentical to the template (an init-only file: present)
OUTDATEDdiffers from the (rendered) template: sync overwrites it
MISSINGabsent: sync (or init) creates it
SKIPPEDproject-owned through [scaffold] skip: never written, not drift
SYMLINKCONTRIBUTING.md is not a link to its target: sync makes it one
PATCHa required configuration fragment is missing: sync inserts it
MANUALthe fragment cannot be inserted unambiguously: sync prints it for you to add
UNRENDEREDa project-owned file still holds placeholders: fill them, or delete it and run init
  • No consent, no write. sync asks per file (Enter means yes); when no answer is possible (no terminal, stdin at end of file, CI, a pipe) the answer is no. Unattended runs need --yes.
  • init creates src/, docs/, .github/workflows/, .github/actions/ and scripts/, then every missing managed file; an existing file is left alone (exists). It asks for the project variables it cannot resolve; --yes asks nothing and leaves them empty.
  • Plain status exits 0 whatever drifts; only --strict turns drift into exit 1. Pin the FoBiS version in that CI job: a FoBiS upgrade with new templates is drift.
  • Stateless: drift is the SHA-256 of the file against the SHA-256 of the rendered template, nothing recorded. Files outside the manifest are never touched.
  • A file it cannot write (permissions) stops the command with Python's PermissionError.
  • Then commit by hand: git add -A && git commit -m "chore: sync scaffold boilerplate".

New project from scratch: fobis new (in Building) writes a fobos and sources, and fobis new --scaffold adds this boilerplate.

What scaffold manages ​

CategoryFilessync
verbatim.github/workflows/ci.yml, docs.yml, install.yml, .github/actions/run-coverage-analysis/action.yml, scripts/release.sh, scripts/run_tests.sh, scripts/install.sh, scripts/compute-coverage.sh, docs/package.json, cliff.toml, LICENSE.gpl3.mdoverwrites
templated.github/workflows/release.yml (), .github/actions/setup-build-env/action.yml ()overwrites with the rendered text
init-onlyfobos, fpm.toml, docs/ford.md (rendered when created), docs/guide/contributing.mdnever: yours after init
patcheddocs/.vitepress/config.mts (rendered when created)only inserts the build: { target: 'es2022' } block into vite: {
symlinkCONTRIBUTING.md -> docs/guide/contributing.mdcreates or repairs the link, never writes through it

What the files do: ci.yml builds and tests; docs.yml deploys the VitePress site and the coverage badge; release.yml publishes the release notes when a tag is pushed; install.yml smoke-tests the install methods the project has (make, CMake, FoBiS, fpm) after a release. scripts/release.sh vX.Y.Z (or --patch/--minor/--major) regenerates CHANGELOG.md with git-cliff, updates VERSION (and the fpm.toml version), commits, tags and pushes. scripts/run_tests.sh runs the tests serially, --np N under MPI, --vmem KB capping each test's memory; a test with a <name>.result file (the doctests) must print that result.

The variables come from the fobos [project] section, then from git:

Variablefobos [project] keyFallback
namethe repository name
summaryempty
repositorygit remote get-url origin (SSH turned into HTTPS)
(derived)the last path segment of
websiteempty
authors, one per line, joined with , git config user.name
emailgit config user.email
year (pin the founding year)the current year
[dependencies], written as fpm dependenciesthe git submodules
ini
[project]
name       = PENF
summary    = Portability Environment for Fortran
version    = VERSION
repository = https://github.com/szaghi/PENF
website    = https://szaghi.github.io/PENF
email      = stefano.zaghi@cnr.it
year       = 2015
authors    = Stefano Zaghi
             Jane Doe

The [scaffold] section holds the project's own knobs; without it every file renders to the defaults:

ini
[scaffold]
; extra system packages for the CI build environment, appended to its apt install line
apt_packages = libopenmpi-dev openmpi-bin zlib1g-dev
; managed files this project owns (globs): reported SKIPPED, never written by sync or init
skip         = scripts/release.sh
  • skip is for a deliberate customisation (a release.sh that also bumps a CMake version): the file stays out of status --strict, sync, init, the dashboard's Drift column and fobis ecosystem scaffold.
  • MPI in CI: the packages go in apt_packages; running the tests under mpirun is scripts/run_tests.sh --np N.
  • Old layouts are replaced: a run-coverage action, an install.sh or a cliff.toml hardcoding the repository, a release.yml that also deployed the docs are all OUTDATED, and sync writes the current files.

Shared boilerplate across the projects ​

The same states for every registered project, honouring each one's skip:

console
$ fobis ecosystem scaffold status
core: in sync
  SKIPPED  scripts/release.sh (project-owned)
mathx: in sync
  SKIPPED  scripts/release.sh (project-owned)
app: in sync
  SKIPPED  scripts/release.sh (project-owned)
console
$ fobis ecosystem scaffold status
core: in sync
  SKIPPED  scripts/release.sh (project-owned)
mathx: 1 not in sync
  OUTDATED .github/workflows/ci.yml
  SKIPPED  scripts/release.sh (project-owned)
app: in sync
  SKIPPED  scripts/release.sh (project-owned)
[exit status 1]
bash
fobis ecosystem scaffold status                        # exit 1 when any project drifts
fobis ecosystem scaffold status --only PENF,FACE --files ".github/*"
fobis ecosystem scaffold sync                          # the diffs, project by project: writes nothing
fobis ecosystem scaffold sync --apply                  # confirm each project (default no), then write
fobis ecosystem scaffold sync --files "scripts/*" --apply --yes
  • --apply without a terminal writes nothing and exits 1: add --yes for an unattended run.
  • After upgrading FoBiS the new templates show as drift: pip install -U FoBiS.py, then scaffold status, scaffold sync to review, scaffold sync --apply, and fobis ecosystem exec -- git status -s before committing in each repository.

The release train ​

Releasing a family by hand means: release the bottom library, wait for it, update its dependents' locks, release them, and so on up the graph. release plans the whole train first:

console
$ fobis ecosystem release --dry-run --no-wait
Release plan (dependencies first)
   1. core  v1.0.0 -> v1.1.0        ready  (minor, 1 releasable commit(s))
        ! CI at HEAD: unknown
        ! not on GitHub: the GitHub release cannot be awaited

Dry run: nothing released.

fobis ecosystem release

  1. The plan. After a git fetch and a CI query, every project with releasable commits, in release order, with the version its commits call for, computed from its latest tag. --bump NAME=major|minor|patch or --bump NAME=vX.Y.Z overrides one (repeatable) and brings into the plan a project with nothing releasable.

  2. Pre-flight, for every project before anything runs. One blocker refuses the whole train, so no ecosystem is left half released:

    Blocks the trainOnly warns
    no executable scripts/release.shunpushed commits (CI has not seen them)
    git-cliff not on the PATHCI running, none or unknown
    not on the trunk (the remote default branch)not fetched (--no-fetch)
    uncommitted changesnot on GitHub (the release cannot be awaited)
    behind the upstream, or no upstream
    CI failing at HEAD
    no previous tag and no explicit version
    version not above the current tag, or already tagged
    an unreadable project
  3. Confirmation of the plan as a whole (default no).

  4. For each project: scripts/release.sh vX.Y.Z with the planned version, so the version you approved is the one tagged; it runs with your terminal's stdin and asks its own confirmation. Then the tag must exist locally and on the remote; the GitHub release is awaited (gh release view, --timeout, default 900 s); and fobis fetch --update runs in its direct dependents, so the next projects release against the new version.

  5. Stop at the first failure, the rest not run, printing the command that resumes the train: Stopped at FLAP. Fix it, then resume with: followed by fobis ecosystem release --from FLAP, with your --bump overrides repeated.

console
$ fobis ecosystem release --yes --no-wait
Release plan (dependencies first)
   1. core  v1.0.0 -> v1.1.0        ready  (minor, 1 releasable commit(s))
        ! CI at HEAD: unknown
        ! not on GitHub: the GitHub release cannot be awaited

==> core: ./scripts/release.sh v1.1.0
  Current version : v1.0.0
  New version     : v1.1.0
Released v1.1.0!

==> mathx: fobis fetch --update
Updating core
Saved deps config to third_party/.deps_config.ini
Saved lock file to third_party/fobos.lock

==> app: fobis fetch --update
Updating mathx
Updating core
Saved deps config to third_party/.deps_config.ini
Saved lock file to third_party/fobos.lock

Project  Result        Time  Detail
core     passed        0.0s  v1.1.0 pushed (GitHub release not awaited)

1 passed, 0 failed
console
$ fobis ecosystem dashboard --no-ci
Project  Branch  Tree   Upstream  Version / tag    Unreleased          CI       Drift  Pins
-------  ------  -----  --------  ---------------  ------------------  -------  -----  -------
core     master  clean  in sync   v1.1.0 / v1.1.0  0                   unknown  0      -
mathx    master  clean  in sync   v1.0.0 / v1.0.0  1, none to release  unknown  0      current
app      master  clean  in sync   v1.0.0 / v1.0.0  0                   unknown  0      current
bash
fobis ecosystem release --dry-run                     # the plan and its blockers, nothing else
fobis ecosystem release                               # the train
fobis ecosystem release --only core,mathx --bump core=v2.0.0
fobis ecosystem release --no-wait                     # do not wait for the GitHub releases
  • A declined prompt is no release. release.sh exits 0 when you answer no; the missing tag reports it aborted. Nothing was pushed: re-run the train.
  • Resuming never releases twice: a released project's new tag leaves it nothing to release, so it drops out.
  • --yes approves the plan and answers every release.sh prompt. Use it only for an unattended run of a plan reviewed with --dry-run.
  • The release is awaited only on GitHub with gh on the PATH; otherwise the result reads vX.Y.Z pushed (GitHub release not awaited). A release not published within the timeout, or a failed fetch in a dependent, fails the project.
  • Your own release.sh works if it takes the version as its only argument, tags and pushes; git-cliff is required anyway (pipx install git-cliff), as the scaffolded script needs it.
  • Exit codes: 0 when every planned project is released, nothing needs a release, the plan is declined, or a --dry-run finds no blocker; 1 when a project is blocked, the confirmation cannot be answered, or a release fails, aborts or is not published in time; 2 for an invalid --bump or an unknown project.

What calls for a release ​

Whether a project needs a release is decided by the files its commits change; how big a release by their Conventional Commits type:

  1. A commit since the latest tag is releasable when it changes at least one file outside the excluded globs, by default docs/* .github/* *.md scripts/* (fnmatch on repository-relative paths, * also matching /). A docs-only or CI-only commit is unreleased but calls for no release, whatever its type; a commit changing no file (a merge, an empty commit) does not count.
  2. The bump comes from the releasable commits only: a ! after the type or scope, or a BREAKING CHANGE footer, means major; feat means minor; anything else, a non-conventional message included, means patch.
CommitFiles changedReleasableBump
fix(docs): target es2022docs/package.json, scripts/release.shno
build(fpm): track default branchesfpm.tomlyespatch
feat(core): add halve()src/core_m.f90yesminor
refactor(api)!: rename twicesrc/core_m.f90yesmajor

Commit types are written by hand and are sometimes wrong; the files a commit touches are not. A project replaces the globs in its own fobos (the dashboard and release both read them):

ini
[ecosystem]
release_exclude = docs/* .github/* *.md scripts/* examples/*

A change at the bottom of the stack ​

bash
# edit PENF, do not commit yet
fobis ecosystem impact PENF                 # which dependents use what changed, file by file
fobis ecosystem check PENF                  # do they still build and pass against the working copy?
git -C ~/fortran/PENF commit -am "feat(io): ..." && git -C ~/fortran/PENF push
fobis ecosystem release --dry-run           # PENF, and anything else that needs a release
fobis ecosystem release

A morning look at the family is fobis ecosystem dashboard (what changed, what is red, what is stale), then fobis ecosystem fetch to bring every pin to the current heads.

In CI or a cron job ​

bash
fobis ecosystem dashboard --json > ecosystem.json
fobis ecosystem scaffold status             # exit 1 on drift
fobis ecosystem release --dry-run           # exit 1 when a planned release is blocked

More in Continuous integration.

Try it offline ​

The documentation ships a script that builds a small private ecosystem: three Fortran projects (core <- mathx <- app, and app also uses core), each a clone of a local bare remote, with history, a v1.0.0 tag, fetched dependencies in third_party/, the scaffold boilerplate, a minimal scripts/release.sh, and commits after the tag (core a releasable feat, mathx a docs commit, app nothing). Nothing leaves the directory you give it, so you can release, break and repair at will:

bash
bash docs/demo/ecosystem-demo/setup.sh ~/eco    # the script is in the FoBiS repository
source ~/eco/env.sh                             # the demo registry; its github.com/demo URLs resolve locally
cd ~/eco
fobis ecosystem graph
fobis ecosystem dashboard --no-ci
fobis ecosystem check core
fobis ecosystem release --dry-run
console
$ ls
app
core
env.sh
mathx
remotes
console
$ cat app/fobos | sed -n "/dependencies/,/^$/p"
[dependencies]
deps_dir = third_party
mathx    = https://github.com/demo/mathx
core     = https://github.com/demo/core

env.sh sets XDG_CONFIG_HOME to the demo's own configuration, so your registry is never touched, and rewrites https://github.com/demo/ to the local remotes with git's url.<base>.insteadOf, so fetch and release work without network. It needs git and gfortran, and git-cliff for the release pre-flight. Every output on this page and every GIF was recorded on it. Delete the directory to remove it.

Configuration at a glance ​

In the user configuration (~/.config/fobis/config.ini):

ini
[ecosystem]
root     = ~/fortran
projects = PENF FACE BeFoR64 StringiFor
           FLAP
theme    = github
mode     = auto

In each project's fobos:

ini
[ecosystem]
; the commands of fobis ecosystem check, one per line
check           = fobis build --mode tests-gnu
                  ./scripts/run_tests.sh
; changes that call for no release
release_exclude = docs/* .github/* *.md scripts/*

[scaffold]
apt_packages = libopenmpi-dev
skip         = scripts/release.sh

The [project] keys feed the templates (above) and version, the dashboard's Version column; the [dependencies] section is the graph (Dependencies).