{"id":51339644,"url":"https://github.com/async/pipeline","last_synced_at":"2026-07-02T06:04:45.008Z","repository":{"id":362548941,"uuid":"1259661122","full_name":"async/pipeline","owner":"async","description":"Local-first TypeScript pipelines for tasks, jobs, cache, and runners","archived":false,"fork":false,"pushed_at":"2026-06-19T23:43:05.000Z","size":2310,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-20T01:11:44.623Z","etag":null,"topics":["async","cd","ci","github-actions","pipeline"],"latest_commit_sha":null,"homepage":"https://async.github.io/pipeline/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/async.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-04T18:22:43.000Z","updated_at":"2026-06-19T23:43:08.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/async/pipeline","commit_stats":null,"previous_names":["async-framework/async-pipeline","async/pipeline"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/async/pipeline","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fpipeline","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fpipeline/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fpipeline/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fpipeline/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/async","download_url":"https://codeload.github.com/async/pipeline/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/async%2Fpipeline/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35034985,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-02T02:00:06.368Z","response_time":173,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","cd","ci","github-actions","pipeline"],"created_at":"2026-07-02T06:04:44.364Z","updated_at":"2026-07-02T06:04:44.989Z","avatar_url":"https://github.com/async.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @async/pipeline\n\nWrite the workflow in TypeScript, run it locally, and generate the thin GitHub Actions bootloader from the same `pipeline.ts`.\n\n`@async/pipeline` is a small TypeScript pipeline engine for projects that want their everyday verification flow to be local-first instead of CI-only. Put the task graph in `pipeline.ts`, run it on your laptop with `async-pipeline`, and let GitHub Actions call the same graph with a thin workflow.\n\n## Why Use It\n\n- Replace duplicated local scripts and CI-only YAML logic with one typed `pipeline.ts`.\n- Run the same task graph on a laptop and in GitHub Actions.\n- Generate and check a pinned GitHub Actions workflow from `pipeline.ts`.\n- Keep run records, logs, summaries, source checkouts, and task cache under `.async/`.\n- Make cache behavior explicit through declared task inputs, cache refs, and task config.\n- Give people and agents inspectable commands: `list`, `graph`, `explain`, `metadata`, `matrix`, and `doctor`.\n- Run many-repo impact checks with explicit dependent repos and namespaced task refs such as `storefront:test`.\n- Read pipeline metadata without cloning sources, running `prepare`, executing tasks, or evaluating deferred shell callbacks.\n- Keep GitHub Actions pinned, low-permission, and focused on invoking the local pipeline.\n\n## Syncs Without Taking Over\n\n`pipeline.ts` is the single source of truth, but GitHub Actions and package-manager manifests are never taken over — they receive only what you opt into through `sync`:\n\n```txt\n                 ┌─ run ──────▶  your laptop and CI, evidence under .async/\n pipeline.ts ────┤\n (the source)    └─ sync ─────▶  .github/workflows/async-pipeline.yml   (thin bootloader)\n                    opt-in,  ─▶  package scripts or Deno tasks          (namespaced, locked)\n                    checked\n```\n\nGitHub Actions keeps triggers, runners, permissions, and secrets; it stops being where workflow logic lives. Package scripts and Deno tasks stay readable aliases; sync writes only namespaced, lock-owned entries and fails on collisions instead of overwriting yours. `sync check` fails CI when either surface drifts from `pipeline.ts`, and leaving is two deletions: the `sync` block and the generated files. The full story: [docs/sync.md](docs/sync.md).\n\n## Quick Start\n\nTry the repo's own pipeline (requires Node \u003e= 24 and Deno \u003e= 2 on macOS or Linux; `pipeline.ts` loads natively):\n\n```sh\ngit clone https://github.com/async/pipeline.git\ncd async-pipeline\npnpm install --frozen-lockfile\npnpm run build\npnpm run pipeline:verify\n```\n\nInspect the run:\n\n```sh\nls .async/runs\ncat .async/runs/\u003crun-id\u003e/summary.md\ncat .async/runs/\u003crun-id\u003e/execution.json\n```\n\nThe self pipeline lives in [pipeline.ts](pipeline.ts). It runs `build`, `typecheck`, `test`, and `pack` through the `verify` job, and it declares the GitHub triggers used to generate [.github/workflows/async-pipeline.yml](.github/workflows/async-pipeline.yml). The initial `pnpm run build` in the quickstart bootstraps the built CLI that loads `pipeline.ts`; after that, the pipeline owns the task order.\n\n## Examples\n\nSee [examples](examples/README.md) for copyable pipeline shapes, all exercised by this repo's own `release:check`: a [basic node package](examples/basic-node-package/README.md), [generated package previews](examples/generated-package-previews/README.md), the [GitHub-native npm preview package workflow](examples/github-native-npm-preview-package/README.md), [monorepo package selection](examples/monorepo-package-selection/README.md), a [Deno-only pipeline](examples/deno-only-pipeline/README.md), a [Deno worker](examples/deno-worker/README.md), a [many-repo impact run](examples/many-repo-impact-run/README.md), a [custom cache registry](examples/custom-cache-registry/README.md), and a [runtime middleware stack](examples/runtime-middleware-stack/README.md).\n\n## How It Compares\n\n`@async/pipeline` sits between package-manager scripts and full monorepo build systems. Use it when the workflow graph itself should be typed, inspectable, local-first, and reusable by CI.\n\n| Tool | Best fit | How `@async/pipeline` differs |\n| --- | --- | --- |\n| Turborepo / Nx | Mature monorepo task orchestration, affected-package logic, parallel scheduling, and ecosystem integrations. | Smaller and explicit: developers declare the graph and inputs, metadata can be inspected safely, and dependency discovery is not inferred. |\n| npm / pnpm scripts | Simple command aliases and package-local workflows. | Adds typed tasks, declared inputs and outputs, cache records, run logs, graph inspection, and generated CI. |\n| GitHub Actions | Hosted CI, permissions, environments, platform events, and hosted runners. | Keeps GitHub Actions as a pinned bootloader that invokes the same local graph instead of redefining workflow logic in YAML. |\n\nChoose Turborepo or Nx for large monorepos that need advanced scheduling and affected-project automation. Choose npm or pnpm scripts for one-off aliases. Choose GitHub Actions directly for CI-only workflows. Choose `@async/pipeline` when task graph metadata, local run evidence, and thin generated CI matter most.\n\n## Add A Pipeline\n\nAfter the package is published, install the public package:\n\n```sh\npnpm add -D @async/pipeline\n```\n\nDeno-only repos can omit `package.json` and run the published CLI through Deno's npm compatibility layer:\n\n```sh\ndeno run -A npm:@async/pipeline/cli run verify\n```\n\nDeno support relies on Deno's npm and `node:` compatibility layer for the pipeline package internals; see the [Deno Node/npm compatibility docs](https://docs.deno.com/runtime/fundamentals/node/).\n\nCreate `pipeline.ts`:\n\n```ts\nimport { definePipeline, job, sh, task, trigger } from \"@async/pipeline\";\n\nexport default definePipeline({\n  name: \"app\",\n  cache: \"file:local\",\n  triggers: {\n    pr: trigger.github({ events: [\"pull_request\"] }),\n    main: trigger.github({ events: [\"push\"], branches: [\"main\"] }),\n    nightly: trigger.cron(\"17 2 * * *\")\n  },\n  sync: {\n    github: true,\n    tasks: true\n  },\n  namedInputs: {\n    source: [\"src/**/*.ts\", \"package.json\", \"pnpm-lock.yaml\", \"tsconfig.json\"]\n  },\n  tasks: {\n    typecheck: task({\n      inputs: [\"source\"],\n      cache: \"file:local\",\n      run: sh`pnpm run typecheck`\n    }),\n    test: task({\n      dependsOn: [\"typecheck\"],\n      inputs: [\"source\"],\n      cache: \"file:local\",\n      run: sh`pnpm run test`\n    }),\n    build: task({\n      dependsOn: [\"test\"],\n      inputs: [\"source\"],\n      outputs: [\"dist/**\"],\n      cache: \"file:local\",\n      run: sh`pnpm run build`\n    })\n  },\n  jobs: {\n    verify: job({ target: \"build\", trigger: [\"pr\", \"main\"] }),\n    nightly: job({ target: \"build\", trigger: [\"nightly\"] })\n  }\n});\n```\n\nThe mental model is deliberately small:\n\n```txt\ntasks     = what can run\njobs      = named entrypoints\ntriggers  = when jobs should run\nsync      = generated files to keep current\n```\n\nTriggers describe when jobs should run. Sync describes which generated files should be kept current.\n\nNested task groups flatten with `.`: `claims.default` runs as `claims`, and `claims.report` runs as `claims.report`. A helper package can return a nested task object and the host pipeline decides where to mount it:\n\n```ts\nfunction claimsTasks({ task, sh }) {\n  return {\n    default: task({ run: sh`async-claims check` }),\n    report: task({ run: sh`async-claims check --format json --no-fail` })\n  };\n}\n\nexport default definePipeline({\n  name: \"app\",\n  tasks: {\n    claims: claimsTasks({ task, sh })\n  },\n  jobs: {\n    verify: job({ target: \"claims\" })\n  }\n});\n```\n\nSource namespaces still use `:`, so `storefront:claims.report` means task `claims.report` from source `storefront`.\n\nAdd scripts manually, or let task sync write package-manager commands for selected jobs:\n\n```json\n{\n  \"scripts\": {\n    \"async-pipeline\": \"async-pipeline\",\n    \"verify\": \"async-pipeline run verify\"\n  }\n}\n```\n\nAdd local pipeline state to `.gitignore`:\n\n```gitignore\n.async/\n*.tgz\n.tmp/\n```\n\nKeep the generated GitHub workflow and lock committed:\n\n```txt\n.github/workflows/async-pipeline.yml\n.locks/pipeline/github-workflow.lock.json\n.locks/pipeline/tasks.lock.json\n```\n\nRun the same graph locally:\n\n```sh\npnpm run pipeline:verify\n```\n\n## Useful Commands\n\n```sh\nasync-pipeline list\nasync-pipeline run \u003cjob\u003e [--concurrency \u003cn\u003e] [--force] [--dry-run] [--format text|json]\nasync-pipeline run-task \u003ctask\u003e [--concurrency \u003cn\u003e] [--force] [--dry-run] [--format text|json]\nasync-pipeline graph --format json\nasync-pipeline graph --format dot\nasync-pipeline explain \u003ctask\u003e\nasync-pipeline metadata --format json\nasync-pipeline sources list\nasync-pipeline sources sync\nasync-pipeline matrix \u003cjob\u003e --format github\nasync-pipeline sync list\nasync-pipeline sync generate\nasync-pipeline sync check\nasync-pipeline sync github list\nasync-pipeline sync github generate [--workflow \u003cpath\u003e] [--lock \u003cpath\u003e]\nasync-pipeline sync github check [--workflow \u003cpath\u003e] [--lock \u003cpath\u003e]\nasync-pipeline sync tasks list\nasync-pipeline sync tasks generate\nasync-pipeline sync tasks check\nasync-pipeline github generate [--workflow \u003cpath\u003e] [--lock \u003cpath\u003e]\nasync-pipeline github check [--workflow \u003cpath\u003e] [--lock \u003cpath\u003e]\nasync-pipeline github plan [--job \u003cid\u003e] [--event \u003cname\u003e] [--event-action \u003caction\u003e] [--format text|json]\nasync-pipeline github run [--job \u003cid\u003e] [--event \u003cname\u003e] [--event-action \u003caction\u003e] [--network mock|deny|allow] [--dry-run] [--format text|json]\nasync-pipeline signoff create [context...] [--job \u003cid\u003e] [--run latest|\u003cid\u003e] [--sha \u003cref\u003e] [--context \u003cname\u003e]\nasync-pipeline signoff status [context...] [--job \u003cid\u003e] [--sha \u003cref\u003e] [--local-only|--remote-only] [--format text|json]\nasync-pipeline signoff revoke [context...] [--job \u003cid\u003e] [--sha \u003cref\u003e] [--reason \u003ctext\u003e]\nasync-pipeline signoff check [context...] [--job \u003cid\u003e] [--sha \u003cref\u003e] [--local-only|--remote-only] [--format text|json]\nasync-pipeline lifecycle audit [--package \u003cpath\u003e] [--format text|json]\nasync-pipeline cache clear\nasync-pipeline gc [--keep \u003cn\u003e] [--cache-days \u003cn\u003e]\nasync-pipeline doctor\n```\n\nThe scheduler starts ready tasks in deterministic graph order and runs independent tasks in parallel up to the configured concurrency. Use `--concurrency 1` when a run needs strict sequential execution. `--force` re-runs tasks while still recording fresh cache entries, `--dry-run` prints the plan with predicted cache hits without executing, `cache clear` resets the task cache, and `gc` prunes old run records and cache entries unused for `--cache-days` days (20 run records and 30 cache days by default). Runs also auto-prune to the newest 50 records; set `ASYNC_PIPELINE_KEEP_RUNS` to change the limit or `0` to disable. Task output buffers cap at 8 MiB per stream (`ASYNC_PIPELINE_MAX_LOG_BYTES`, `0` = unlimited); stored logs keep the tail. The CLI finds `pipeline.ts`, `pipeline.js`, `pipeline.mjs`, or `pipeline.mts` from any subdirectory by walking up.\n\nUse `async-pipeline` as the explicit command in docs and CI. Short aliases and smart runner dispatch belong in `@async/run`, not this package.\n\n`signoff create` posts an advisory `async/local/\u003cjob\u003e` GitHub commit status only after it finds a passed local Pipeline run recorded for the selected commit SHA. It refuses dirty or unpushed commits by default, writes a bounded local receipt under `.async/signoff/\u003csha\u003e/`, and leaves branch protection and rulesets unchanged.\n\nUse `lifecycle audit` before migrating a repo's release or publish flow. It reads package manifests, Pipeline config and locks, lifecycle-looking package scripts, generated or custom workflow filenames, release config files, and lifecycle-looking files under `scripts/` without executing repo commands or mutating files. Text output is designed for quick review; `--format json` is stable enough for ADR receipts and migration dashboards.\n\n## GitHub Actions\n\nGitHub Actions requires committed YAML for `push`, `pull_request`, `schedule`, `release`, and `workflow_dispatch`. `@async/pipeline` keeps that YAML as a generated bootloader:\n\n```sh\nasync-pipeline github generate\n# or\nasync-pipeline sync github generate\n```\n\nThat writes:\n\n```txt\n.github/workflows/async-pipeline.yml\n.locks/pipeline/github-workflow.lock.json\n```\n\nFor tests or local experiments, render somewhere else:\n\n```sh\nasync-pipeline github generate --workflow .tmp/async-pipeline.yml --lock .tmp/async-pipeline.lock.json\nasync-pipeline github check --workflow .tmp/async-pipeline.yml --lock .tmp/async-pipeline.lock.json\n```\n\nThe generated workflow installs dependencies, checks that the YAML and lock still match `pipeline.ts`, and delegates job selection back to the CLI:\n\n```sh\nasync-pipeline github check\nasync-pipeline github run [--concurrency \u003cn\u003e]\n```\n\nUse `github plan` to inspect the stable JSON manifest for generated jobs before a live workflow runs:\n\n```sh\nasync-pipeline github plan --job verify --event pull_request --format json\nasync-pipeline github plan --job contract --event pull_request --format json\n```\n\nUse `github run` with `--event`, `--event-action`, and `--network mock|deny|allow` to simulate selected generated jobs locally; it writes manifests, per-step JSON, artifact directories, and receipts under `.async/github-local/`. Generated contract lanes from `sync.github.contract` and hygiene lanes from `sync.github.hygiene` can be planned and run locally with `--network deny` because pipeline owns the event policy and the actions write bounded evidence under `.async/contract` and `.async/hygiene`.\n\nThe checked-in generated workflow is [.github/workflows/async-pipeline.yml](.github/workflows/async-pipeline.yml).\n\n## Package Task Sync\n\n`sync.tasks: true` syncs all pipeline jobs into the root package-manager manifest. It writes package `scripts` in `package.json` and Deno `tasks` in `deno.json` or `deno.jsonc`:\n\n```json\n{\n  \"scripts\": {\n    \"pipeline:verify\": \"async-pipeline run verify\"\n  }\n}\n```\n\nRaw task commands are opt-in and namespaced:\n\n```ts\nsync: {\n  tasks: {\n    prefix: \"pipeline\",\n    runners: [\"package\"],\n    targets: [{ package: \"@acme/app\" }],\n    jobs: [\"verify\"],\n    tasks: [\"typecheck\"],\n    scripts: {\n      \"sync:check\": \"sync check\"\n    }\n  }\n}\n```\n\nThat can generate:\n\n```json\n{\n  \"scripts\": {\n    \"pipeline:verify\": \"async-pipeline run verify\",\n    \"pipeline:task:typecheck\": \"async-pipeline run-task typecheck\",\n    \"pipeline:task:claims.report\": \"async-pipeline run-task claims.report\",\n    \"pipeline:sync:check\": \"async-pipeline sync check\"\n  }\n}\n```\n\nTask sync records ownership in `.locks/pipeline/tasks.lock.json`. `sync tasks generate` never overwrites an existing unmanaged script or Deno task. If a generated command exists but is not claimed by the lock, it fails with `ASYNC_PIPELINE_SYNC_CONFLICT`. Checks still read the legacy `.async-pipeline/tasks.lock.json` path while repos migrate.\n\nDeno-only roots with `deno.json` or `deno.jsonc` and no `package.json` default generated task commands to `deno run -A npm:@async/pipeline/cli`; set `sync.command` when you want a local wrapper such as `deno task async-pipeline`.\n\n## Cache Registry\n\nThe default pipeline cache registry includes `file` and `memory`. `cache: true` uses the pipeline default, and explicit refs make task behavior easy to read:\n\n```ts\ntask({ cache: \"file:local\", run: sh`pnpm run test` })\n```\n\nCache keys are derived from the task config, resolved commands, declared inputs, direct dependency cache fingerprints, and portable source/candidate metadata. Input resolution ignores `.git/`, `.async/`, and `node_modules/` by default, and a task's declared `outputs` are excluded from that task's inputs so build artifacts do not dirty their own cache entry.\n\nWhen a cached task declares `outputs`, the runner stores a validated output manifest and an output blob next to `result.json`, then restores those files before returning a cache hit. `file` persists those blobs under `.async/cache/tasks`; `memory` keeps them process-local; `customCache({ adapter })` lets callers provide another blob store with only `get` and `put`. `ttlMs` is enforced when present; expired entries rerun.\n\nYou can override the registry without adding Redis or remote cache dependencies to this package:\n\n```ts\nimport { customCache, defineCache, definePipeline, fileCache, job, sh, task } from \"@async/pipeline\";\n\nconst remoteAdapter = {\n  async get(key) {\n    return await readFromRemoteCache(key);\n  },\n  async put(key, value) {\n    await writeToRemoteCache(key, value);\n  }\n};\n\nconst caches = defineCache({\n  default: \"file:local\",\n  stores: {\n    file: fileCache({ root: \".async/cache/tasks\" }),\n    remote: customCache({ adapter: remoteAdapter })\n  }\n});\n\nexport default definePipeline({\n  name: \"app\",\n  cache: caches,\n  tasks: {\n    test: task({ cache: \"file:local\", run: sh`pnpm run test` })\n  },\n  jobs: {\n    verify: job({ target: \"test\" })\n  }\n});\n```\n\nAdapters are deliberately dumb blob stores: pipeline computes keys, writes manifests, enforces TTL, validates restored outputs, and records hit/miss receipts. `get` and `put` are the only required adapter methods; built-in stores also expose optional `list`, `delete`, `touch`, and `prune` hooks for deeper cache integration. `redisCache(...)` uses a user-supplied `redis://` or `rediss://` URL through Node's built-in socket APIs, so no Redis npm client is required.\n\n## Many-Repo Impact Runs\n\nDeclare known dependent repos yourself:\n\n```ts\nimport { definePipeline, job, sh, source, task } from \"@async/pipeline\";\n\nexport default definePipeline({\n  name: \"design-system\",\n  sources: {\n    storefront: source.git({\n      url: \"https://github.com/acme/storefront.git\",\n      ref: \"main\",\n      prepare: [\n        sh`pnpm install --frozen-lockfile`,\n        sh((ctx) =\u003e sh`pnpm add @acme/design-system@file:${ctx.candidate.dir}`)\n      ]\n    })\n  },\n  tasks: {\n    impact: task({ dependsOn: [\"storefront:test\"] })\n  },\n  jobs: {\n    verifyImpact: job({ target: \"impact\" })\n  }\n});\n```\n\nHow it works:\n\n- `source.git(...)` declares the repo, ref, and pipeline file to compose.\n- The CLI clones git sources into `.async/sources/\u003csource-id\u003e/\u003chash\u003e` when you run `async-pipeline sources sync`, `async-pipeline run \u003cjob\u003e`, or `async-pipeline run-task \u003csource\u003e:\u003ctask\u003e`.\n- The hash is derived from the source URL and ref, so repeated runs reuse the same warm checkout.\n- `prepare` runs inside that source checkout before source tasks run.\n- `ctx.candidate.dir` points back to the root repo being tested, which lets the source checkout install or link the candidate change.\n- Use `source.path(...)` instead when you want to point at a specific local checkout yourself.\n\n`@async/pipeline` does not infer reverse dependencies from package manifests, lockfiles, npm metadata, or GitHub search. The dependency map stays explicit and reviewable.\n\nPin `ref` to a commit SHA for reproducible impact runs. A branch name like `\"main\"` is convenient while iterating, but it moves underneath you: two runs of the same pipeline can test different dependent code.\n\n## Platform Support\n\nNode projects default generated GitHub workflows to `node@24`; Deno-only projects with `deno.json` or `deno.jsonc` and no `package.json` default to `deno@2`; mixed projects can set `sync.github.runtime` to both. The checked-in workflow targets GitHub-hosted Linux (`ubuntu-latest`) and macOS (`macos-latest`) runners; self-hosted label sets such as Tart-backed Apple Silicon runners are supported through `runsOn`/`runsOnMatrix` (see [GitHub Actions setup](docs/github-actions.md)). Windows is untested; use WSL.\n\n## Use It When\n\n- You want local verification to be the source of truth.\n- CI should invoke, not redefine, your project workflow.\n- You need typed task dependencies, cache inputs, retries, timeouts, requirements, and run records.\n- You want metadata and graph inspection for humans, tools, and AI agents.\n- You own the list of repos that should be checked against a candidate change.\n\n## Not Yet For\n\n- Redis lifecycle management. `redisCache(...)` can use a Redis instance you provide, but it does not start, migrate, or prune Redis for you.\n- Automatic dependency discovery. Sources are explicit by design.\n- Automatic sandbox routing. Isolation is opt-in: select it with `--sandbox`, `--execution`, or programmatic run options; `sandbox.container(...)` is portable OCI image intent, while Docker, Apple container, and Lima are provider choices.\n- Ollama runtime integration. It can be declared as an optional tool requirement, but it is not a package dependency.\n\n## Releases, Snapshots, And The npm Fallback\n\nPublishing runs through generated GitHub Actions from the same `pipeline.ts` that verifies the repo. `@async/pipeline` owns triggers, job graph, matrices, permissions, and generated workflow locks; `async/actions` owns networked publish, preview, release, and Pages step behavior. The model is PatrickJS's [GitHub-native npm preview packages Gist](https://gist.github.com/PatrickJS/3fa2925713fcdf75a27a505ce2cd0d80), dogfooded (the standalone generated-preview example lives in [examples/generated-package-previews](examples/generated-package-previews)):\n\n- Stable releases publish to GitHub Packages as `@async/pipeline` before npm, so a stable version exists on the fallback registry even when npm publishing has an issue.\n- Stable release jobs create or verify the matching `v\u003cversion\u003e` Git tag and GitHub Release through `async/actions/publish` before package publishing, and refuse to move an existing tag.\n- Package-owned release evidence stays in ordinary tasks: put a deterministic `release-evidence` task before `release ensure` or `release sync-descriptions` with `dependsOn`, write the rendered Markdown into the current `CHANGELOG.md` section, and generated workflows preserve that order without pipeline knowing package-specific metrics.\n- Pushes to `main` that pass the verify chain publish an immutable `0.0.0-main.sha.\u003csha\u003e` snapshot to GitHub Packages and move the `main` dist-tag.\n- Same-repo pull requests publish an immutable `0.0.0-pr.\u003cn\u003e.sha.\u003csha\u003e` preview and move the `pr-\u003cnumber\u003e` dist-tag; fork pull requests never publish previews. Previews build the PR merge commit and are stamped with the PR head SHA.\n- Generated preview install comments use `async/actions/comment` with an explicit `GITHUB_TOKEN`, a stable marker, and a same-repo pull request guard.\n- Republishing an existing version skips cleanly instead of failing, so re-dispatched publish jobs stay green.\n\nGitHub Packages requires the package scope to match the repo owner, so the mirror is `@async/pipeline` on `npm.pkg.github.com` while npm publishes the same package name on `registry.npmjs.org`:\n\n```sh\n# One-time GitHub Packages auth (classic PAT with read:packages), plus\n# @async:registry=https://npm.pkg.github.com in your npm config.\nnpm login --scope=@async --auth-type=legacy --registry=https://npm.pkg.github.com\n\n# Stable fallback when npm is unavailable:\npnpm add @async/pipeline@latest\n\n# Latest main snapshot, or a PR preview:\npnpm add @async/pipeline@main\npnpm add @async/pipeline@pr-123\n```\n\n## Docs\n\n- [Docs home](docs/index.md)\n- [Sync: choose what GitHub and package managers see](docs/sync.md)\n- [Getting started](docs/getting-started.md)\n- [npm release MVP](docs/npm-release-mvp.md)\n- [Preview packages MVP](docs/preview-packages-mvp.md)\n- [GitHub Pages MVP](docs/github-pages-mvp.md)\n- [Package repo MVP](docs/package-repo-mvp.md)\n- [How it works](docs/how-it-works.md)\n- [Running locally](docs/local-runs.md)\n- [GitHub Actions setup](docs/github-actions.md)\n- [API reference](docs/api.md)\n- [Many-repo impact runs](docs/many-repo-impact-runs.md)\n- [Path to 1.0](docs/path-to-1.0.md)\n\n## Runtime Primitives\n\nThe MVP remains `pipeline.ts`, local runs, and generated GitHub Actions. The package also exposes additive runtime primitives under `@async/pipeline/runtime` for embeddable workflows:\n\n```ts\nimport { compose, createRuntime, defineRuntime, parallel, task } from \"@async/pipeline/runtime\";\n\nconst work = defineRuntime([\n  task({ id: \"verify\" }, compose(\n    async (ctx, next) =\u003e {\n      ctx.state.started = true;\n      return next();\n    },\n    [\n      async (_ctx, next) =\u003e next(),\n      async (_ctx, next) =\u003e next()\n    ],\n    parallel([\n      async () =\u003e \"typecheck\",\n      async () =\u003e \"test\"\n    ])\n  ))\n]);\n\nconst runtime = createRuntime(work);\nawait runtime.run();\n```\n\n`compose(...)` is public for reusable runtime flows. `task(...)` remains the opinionated boundary for ids, dependencies, cache, inspection, and structured failures.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasync%2Fpipeline","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fasync%2Fpipeline","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasync%2Fpipeline/lists"}