Collect scattered config files from your monorepo and generate them where your systems expect.
Many tools require config files in specific locations:
- GitHub reads
CODEOWNERSfrom.github/CODEOWNERS - GitHub Actions workflows must live in
.github/workflows/ - And more...
In a monorepo, each package has its own context. But these systems only look at root-level paths. You end up with a single massive file that every team has to edit, leading to merge conflicts and unclear ownership.
pull-up lets you keep config files next to the code they describe, then collects and generates them to the locations your systems expect.
packages/
core/
CODEOWNERS # * @core-team
web/
CODEOWNERS # * @frontend-team
api/
CODEOWNERS # * @backend-team
↓ pullup sync
.github/
CODEOWNERS # All entries merged with correct paths
npm install -D @pull-up/cli
# or
yarn add -D @pull-up/cli
# or
pnpm add -D @pull-up/cliCreate pullup.yml or pullup.yaml in your project root:
jobs:
codeowners:
type: codeownersjobs maps job identifiers to their settings. YAML configuration is validated before jobs run; invalid settings cause the command to exit with code 1.
pullup.yaml is searched before pullup.yml.
Collects CODEOWNERS files from your monorepo and merges them into a single file.
jobs:
codeowners:
type: codeowners
input:
- '**/CODEOWNERS'
- '!**/fixtures/**'
output: .github/CODEOWNERS| Field | Type | Required | Default |
|---|---|---|---|
type |
codeowners |
Yes | |
input |
string[] |
No | ['**/CODEOWNERS'] |
output |
string |
No | .github/CODEOWNERS |
Input patterns and the output path are relative to the repository root. Prefix an input pattern with ! to exclude matching files. The output file is automatically excluded from the input.
Use type: custom to generate a file with a shell command:
jobs:
ownership:
type: custom
command: 'python3 scripts/ownership.py'
input: ['packages/**/ownership.yaml']
output: .github/CODEOWNERS| Field | Type | Required | Default |
|---|---|---|---|
type |
custom |
Yes | |
command |
string |
Yes | |
input |
string[] |
No | [] |
output |
string |
Yes |
The command runs through sh -c from the root, so shell syntax such as pipes is supported. It receives a single JSON object on stdin:
{
"sources": [
{
"path": "packages/core/ownership.yaml",
"contents": "owners: ['@core-team']\n"
}
],
"context": {
"rootDir": "/project",
"outputPath": "/project/.github/CODEOWNERS"
}
}Source paths are relative to the repository root, and their contents are UTF-8 text. Both context paths are absolute.
Write the generated file contents to stdout and exit with code 0. Trailing newlines are preserved. Use stderr for error messages and exit with a non-zero code on failure.
The command runs on every pullup sync, pullup check, and pullup sync --dry-run. pull-up writes the generated file only during sync without --dry-run.
The package includes schema.json for autocomplete and validation in editors that support YAML Language Server. In VS Code, install the YAML extension and add this line at the top of your config:
# yaml-language-server: $schema=./node_modules/@pull-up/cli/schema.jsonThe path is relative to the YAML file. For Yarn Plug'n'Play or a remote schema, use https://unpkg.com/@pull-up/cli@<version>/schema.json, replacing <version> with your installed package version.
Starting in 0.0.8, JavaScript and TypeScript configuration files are deprecated. They continue to work, but pull-up prints one warning to stderr per command when it loads a .js, .ts, .cjs, or .mjs configuration file, including .pulluprc files with these extensions. See Migration to move to YAML.
The package.json pullup field and JSON/YAML .pulluprc configurations remain supported without this deprecation warning.
Use pullup.config.ts, pullup.config.js, pullup.config.cjs, or pullup.config.mjs for a JavaScript or TypeScript configuration:
import { defineConfig, codeownersJob } from '@pull-up/cli';
export default defineConfig(codeownersJob());The package.json pullup field and .pulluprc files are also supported. These formats are searched before pullup.yaml and pullup.yml.
Use defineJob to implement a custom transform in a JavaScript or TypeScript config:
import { defineConfig, defineJob } from '@pull-up/cli';
const myJob = defineJob({
name: 'my-job',
input: ['packages/*/config.json'],
output: 'merged-config.json',
transform: (sources) => {
return JSON.stringify(sources.map((s) => JSON.parse(s.contents)));
},
});
export default defineConfig([myJob()]);Transform functions receive source files as { path, contents } objects and a context containing rootDir and outputPath. They can return a string or a promise of a string.
Replace a built-in job in pullup.config.mjs:
import { defineConfig, codeownersJob } from '@pull-up/cli';
export default defineConfig(
codeownersJob({
input: ['**/CODEOWNERS', '!**/fixtures/**'],
output: '.github/CODEOWNERS',
}),
);With pullup.yml:
jobs:
codeowners:
type: codeowners
input: ['**/CODEOWNERS', '!**/fixtures/**']
output: .github/CODEOWNERSFor a custom callback, move the transform into a command. For example, replace this pullup.config.mjs:
import { defineConfig, defineJob } from '@pull-up/cli';
const mergeConfigs = defineJob({
name: 'merge-configs',
input: ['packages/*/config.json'],
output: 'merged-config.json',
transform: (sources) => `${JSON.stringify(sources.map((source) => JSON.parse(source.contents)))}\n`,
});
export default defineConfig([mergeConfigs()]);With pullup.yml:
jobs:
merge-configs:
type: custom
command: 'node scripts/merge-configs.mjs'
input: ['packages/*/config.json']
output: merged-config.jsonCreate scripts/merge-configs.mjs:
process.stdin.setEncoding('utf8');
let input = '';
for await (const chunk of process.stdin) {
input += chunk;
}
const { sources, context } = JSON.parse(input);
try {
const configs = sources.map((source) => JSON.parse(source.contents));
process.stdout.write(`${JSON.stringify(configs)}\n`);
} catch (error) {
console.error(`Failed to generate ${context.outputPath}: ${error.message}`);
process.exitCode = 1;
}The script reads sources and context from JSON on stdin and writes the generated contents, including the final newline, to stdout. It does not write the output file itself. Input patterns and output paths remain relative to the repository root; the command also runs from that root. Your environment must provide the runtime used by the command, such as Node.js in this example.
After moving your jobs, delete the old JavaScript or TypeScript configuration file. Legacy configurations take precedence over pullup.yaml and pullup.yml, so leaving the old file in place prevents pull-up from loading your new YAML configuration. Run pullup sync --dry-run to preview the migrated jobs, then use pullup sync and pullup check as usual.
Generate files from scattered sources:
pullup syncPreview changes without writing:
pullup sync --dry-runVerify generated files are up to date (useful in CI):
pullup check| Option | Description |
|---|---|
--root <path> |
Repository root path |
--cwd <path> |
Working directory |
--dry-run |
Preview without writing (sync only) |