Skip to content

CI Dependency Graph

How GitHub Actions job dependencies are determined and configured.

Core Concept

CI jobs execute in dependency order, not sequentially or by "tiers". Each job declares what it needs:

yaml
test-sql-sqlite:
  needs: [detect, publish-utils, publish-debug, publish-instance]

test-sql-sqlite waits for ALL listed jobs to succeed before starting.

Determining Job Dependencies

Step 1: Identify Module Dependencies

Read the module's loader.js:

javascript
module.exports = function loader (shared_libs, config) {
  const Lib = {
    Utils: shared_libs.Utils,
    Debug: shared_libs.Debug,
    InstanceManager: shared_libs.InstanceManager
  };
  // ...
};

Dependencies: Utils, Debug, InstanceManager

Step 2: Map to Package Names

Lib.*npm PackageCI Publish Job
Utils@superloomdev/js-helper-utilspublish-utils
Debug@superloomdev/js-helper-debugpublish-debug
InstanceManager@superloomdev/js-server-helper-instancepublish-instance

Step 3: Configure Test Job

yaml
test-sql-sqlite:
  needs: [detect, publish-utils, publish-debug, publish-instance]
  if: |
    always() &&
    !cancelled() &&
    needs.detect.result == 'success' &&
    contains(needs.detect.outputs.test_modules, 'js-server-helper-sql-sqlite')

Dependency Depth

Dependencies can be arbitrarily deep:

Module 1 (no deps)

Module 2 (needs Module 1)

Module 3 (needs Module 2)

...

Module 50 (needs Modules 1-49)

CI handles this naturally:

  • publish-module1 runs first
  • publish-module2 waits for publish-module1
  • publish-module3 waits for publish-module2
  • ...
  • test-module50 waits for ALL 49 publish jobs

No "tier 49" needed - just list the dependencies.

Current Superloom Dependency Map

ModuleDependenciesTest Job Needs
utilsNonedetect
debugNonedetect
timeutils, debugpublish-utils, publish-debug
client-cryptoutils, debugpublish-utils, publish-debug
server-cryptoutils, debugpublish-utils, publish-debug
httputils, debugpublish-utils, publish-debug
s3-url-signerutils, debugpublish-utils, publish-debug
instanceutils, debugpublish-utils, publish-debug
sqliteutils, debug, instancepublish-utils, publish-debug, publish-instance
s3utils, debug, instancepublish-utils, publish-debug, publish-instance
dynamodbutils, debug, instancepublish-utils, publish-debug, publish-instance
mongodbutils, debug, instancepublish-utils, publish-debug, publish-instance
sqsutils, debug, instancepublish-utils, publish-debug, publish-instance
mysqlutils, debug, instancepublish-utils, publish-debug, publish-instance
postgresutils, debug, instancepublish-utils, publish-debug, publish-instance
verifyutils, debug, instance, server-cryptopublish-utils, publish-debug, publish-instance, publish-server-crypto

Adding a New Module

1. Identify Dependencies

javascript
// In new-module.js
const Lib = {
  Utils: shared_libs.Utils,
  Postgres: shared_libs.Postgres  // New dependency
};

2. Update CI Workflow

yaml
test-new-module:
  needs: [detect, publish-utils, publish-debug, publish-postgres]
  if: |
    always() &&
    !cancelled() &&
    needs.detect.result == 'success' &&
    contains(needs.detect.outputs.test_modules, 'js-server-helper-new-module')

3. Update Test package.json

json
{
  "dependencies": {
    "@superloomdev/js-helper-utils": "^1.0.0",
    "@superloomdev/js-helper-debug": "^1.0.0",
    "@superloomdev/js-server-helper-sql-postgres": "^1.0.0",
    "@superloomdev/js-server-helper-new-module": "file:../"
  }
}

Common Patterns

Foundation Modules (No Dependencies)

yaml
test-utils:
  needs: [detect]
  # No publish dependencies

Standard Modules (Foundation Dependencies)

yaml
test-time:
  needs: [detect, publish-utils, publish-debug]

Storage Modules (Instance Required)

yaml
test-sqlite:
  needs: [detect, publish-utils, publish-debug, publish-instance]

Complex Modules (Multiple Dependencies)

yaml
test-verify:
  needs: [detect, publish-utils, publish-debug, publish-instance, publish-server-crypto]

The always() Function

yaml
needs: [detect, publish-utils, publish-debug]
if: |
  always() &&
  !cancelled() &&
  needs.detect.result == 'success'

Why always()?

Without it, if a dependency like publish-debug fails, test-time would be skipped entirely.

With always(), the job evaluates its if condition even if dependencies failed. The subsequent checks (!cancelled(), needs.detect.result == 'success') control actual execution.

Job Status Checks

StatusMeaningTest Job Runs?
successJob completed successfullyYes
failureJob failedNo (unless always())
cancelledJob was cancelledNo (blocked by !cancelled())
skippedJob was skippedDepends on if condition

Parallel Execution

Jobs with the same dependencies run in parallel:

yaml
# These all need utils and debug - they run simultaneously
test-time:
  needs: [detect, publish-utils, publish-debug]

test-client-crypto:
  needs: [detect, publish-utils, publish-debug]

test-server-crypto:
  needs: [detect, publish-utils, publish-debug]

GitHub Actions automatically parallelizes independent jobs.

Troubleshooting

"Package not found" Error

Symptom: npm ERR! 404 Not Found - GET https://npm.pkg.github.com/...

Cause: Test job ran before publish job completed.

Fix: Add missing publish-* to needs: array.

"Cannot find module" in Test

Symptom: Error: Cannot find module '@superloomdev/js-helper-utils'

Cause: package.json dependency missing or wrong version.

Fix: Verify package.json has correct package names and version ranges.

Circular Dependencies

Symptom: CI deadlocks or GitHub rejects workflow.

Cause: Module A needs Module B, Module B needs Module A.

Fix: Refactor to eliminate circular dependency. One module must be foundational.

References

Released under the MIT License.