Skip to content
Merged
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
33 changes: 33 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# CLAUDE.md

Guidance for Claude Code working in this repository. Humans should read [README.md](./README.md) and [CONTRIBUTING.md](./CONTRIBUTING.md) first.

## Entry points

- `apify` CLI — `src/entrypoints/apify.ts`
- `actor` CLI — `src/entrypoints/actor.ts`

## Before you push

Run `yarn lint && yarn format && yarn build && yarn test:local` before pushing. Run `yarn test:api` too if you changed anything that touches the Apify API.

If you modified a command's flags, args, description, or added/removed a command, also run `yarn update-docs` and commit the regenerated `docs/` output.

## Code conventions

- Package manager: **Yarn 4** (via Corepack). Do not use npm.
- Use `.js` import specifiers for local files (e.g. `import { foo } from './foo.js'`). The `.ts` source resolves at build time.
- Commands extend `ApifyCommand` from `src/lib/command-framework/apify-command.ts`. Follow the pattern of existing commands: `static override name`, `static override description`, `static override flags/args`, and an `async run()` method.
- New commands must be registered in `src/commands/_register.ts` (or the parent `_index.ts` for subcommands).
- Do not add docstrings, comments, or type annotations to code you did not change. Keep diffs tight.

## Testing

- Tests use **Vitest**. See [CONTRIBUTING.md](./CONTRIBUTING.md#writing-tests) for `useAuthSetup` and `useTempPath` hook usage.
- API tests must include `[api]` in the test name and live in `test/api/`.
- Always `import process from 'node:process'` in command/lib code — never use `globalThis.process`. This is required for test cwd mocks to work.

## Things to avoid

- Do not use `--no-verify` to skip git hooks. Fix the underlying issue.
- Do not edit `docs/` by hand — it is generated by `yarn update-docs`.
187 changes: 109 additions & 78 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,137 +1,168 @@
# Contributing to apify-cli
# Contributing to Apify CLI

## Dev mode
Thanks for your interest in contributing! This guide covers local setup, code style, tests, and the PR process. For a high-level tour of the repo aimed at AI coding assistants, see [CLAUDE.md](./CLAUDE.md).

You can run `yarn dev:apify` to run the CLI in development mode. This will use the local version of the CLI instead of the one installed globally.
## Prerequisites

## Tests
- **Node.js** 22 or higher
- **Yarn 4** (enabled via Corepack — do not install Yarn globally with npm)
- **Bun** ≥ 1.2.5 (optional; only needed if you want to build the standalone bundles locally)
- An [Apify account](https://console.apify.com/) and an API token if you want to run the API test suite

Tests are implemented using the [Vitest](https://vitest.dev/) framework.
You need to have Apify account to test all apify-cli features.
Enable Corepack once per machine:

Then you can run tests with commands in repository root directory:
```bash
corepack enable
```

1. Install all dependencies:
`yarn`
## Local setup

2. Run tests using credentials of the 'apify-test' user:
`TEST_USER_TOKEN=<apifyUserApiToken> yarn test:all`
```bash
git clone https://github.com/apify/apify-cli.git
cd apify-cli
yarn
yarn build
```

## Publish new version
Run the CLI straight from source in dev mode (no global install required):

Only users with access to [apify-cli package](https://www.npmjs.com/package/apify-cli) can publish new version.
```bash
yarn dev:apify --version
yarn dev:apify create my-actor
```

Release of new versions is managed by GitHub Actions. On pushes to the master branch, prerelease versions are automatically produced. Latest releases are triggered manually through the GitHub release tool. After creating a release there, Actions will automatically produce the latest version of the package.
`yarn dev:actor` does the same for the in-Actor `actor` binary.

1. Manually increment version in `package.json`
## Code style

2. GitHub Actions build is triggered by a push to `master` (typically a merge of a PR).
The repo uses three tools, wired together via a pre-commit hook (`husky` + `lint-staged`):

3. To trigger the latest release, go to the GitHub release tool (select `releases` under `<> Code`). There, draft a new release, fill the form and hit `Publish release`. Actions will automatically release the latest version of the package.
- **Biome** — formats JavaScript / TypeScript. Config: `biome.json`.
- **ESLint** — lints JavaScript / TypeScript using `@apify/eslint-config`. Config: `eslint.config.mjs`.
- **Prettier** — formats Markdown and YAML. Config: inherited defaults.

## Writing tests
Run checks manually:

In `test/__setup__/hooks` we have a collection of hooks that you can use while writing tests to set up the testing environment to be usable when running tests in parallel (especially useful for tests that require authenticating into an Apify account).
```bash
yarn lint # ESLint
yarn format # Biome + Prettier (check only)
yarn lint:fix # Auto-fix ESLint issues
yarn format:fix # Auto-format with Biome + Prettier
```

### `useAuthSetup`
The pre-commit hook runs these on staged files automatically. Do not bypass it with `--no-verify` — if a hook fails, fix the underlying issue and create a new commit.

Use this hook at the start of your test file to mark the entire suite as require-ing a separated authentication setup. By default, this will recreate the authentication setup per test in your suite, but you can disable that by passing in `{ perTest: false }` in the call to `useAuthSetup`.`
## Before opening a PR

If you're writing a test case or file that relies on API interactions, make sure it either goes in the `test/api` folder, and that it has a `[api]` reference in the name of the test case (usually at the start).
If your test file also mixes local tests, always add `[api]` for test cases that need the API. They will automatically be skipped for local tests.
Run the full local gauntlet before pushing:

#### Example usage
1. `yarn lint`
2. `yarn format`
3. `yarn build`
4. `yarn test:local` (and `yarn test:api` if relevant — see below)

```typescript
import { useAuthSetup } from "./__setup__/hooks";
If you added, removed, or changed a command's signature, regenerate the reference docs:

useAuthSetup();
// Alternatively, if this suite requires the authentication to persist across all tests
useAuthSetup({ perTest: false });
```bash
yarn update-docs
```

### `useTempPath`
Commit the updated `docs/` output alongside your change.

This hook should always be used when working with commands that alter the file system. This hook:
## Tests

- creates your temporary directory with the name you provided
- provides calls for before and after all tests to setup and clean up the temporary directory
- supports mocking the process cwd to the temporary directory so you can run commands and test their behavior.
Tests use [Vitest](https://vitest.dev/). They fall into four categories:

**Important note about the cwd mocking**: when you use this hook and tell it to mock the cwd, you need to ensure these following things **always** happen:
| Script | What it runs | When to run |
| -------------------- | ------------------------------------------------------------------------ | -------------------------------------------------- |
| `yarn test:local` | Everything except tests named `[api]` and everything outside `test/api/` | Always, on every change |
| `yarn test:api` | Only tests tagged `[api]` — hits the real Apify API | When you touch API-facing code |
| `yarn test:python` | Tests tagged `[python]` — exercises Python Actor templates | When you touch Python template / integration code |
| `yarn test:cucumber` | Cucumber features in `features/` | When you touch anything covered by a `.feature.md` |
| `yarn test:all` | `test:local` then `test:api` | Full pre-release check |

- You import `process` from `node:process` in your command or file you want to test with the mocked cwd. You do not use `globalThis.process` at all!
- You import the files that may rely on the mocked cwd AFTER you call `useTempPath` in your test file, by using `await import()` instead of `import x from '..';`
API tests need a token:

It also comes with several options:
```bash
TEST_USER_TOKEN=<your-apify-token> yarn test:api
```

- `create`: defaulted to `true`, it decides if the temporary directory should be created or not in the beforeAll hook.
- `remove`: defaulted to `true`, it decides if the temporary directory should be removed or not in the afterAll hook.
- `cwd`: defaulted to `false`, it decides if the process.cwd should be mocked to the temporary directory or not.
- `cwdParent`: defaulted to `false`, it decides whether the initial value of the mocked process.cwd will point to the parent directory of the temporary directory or the actual temporary directory.
Use a dedicated test account — API tests create and destroy Actors, datasets, and other resources.

This hook also returns several values in an object:
## Writing tests

- `tmpPath`: the full path to the temporary directory that was requested
- `joinPath`: a utility function similar to `path.join` that lets you work with paths in the temporary directory
- `beforeAllCalls`: a function you should manually call in your `beforeAll` hook to set up the temporary directory as well as the used cwd mock
- `afterAllCalls`: a function you should manually call in your `afterAll` hook to clean up the temporary directory
- `toggleCwdBetweenFullAndParentPath`: a function you can call to toggle the cwd mock between the full path to the temporary directory and the parent directory of the temporary directory
Shared helpers live in `test/__setup__/hooks/`. Two of them show up in most new tests:

#### Example usage (creates an actor, then pushes it, then calls it)
### `useAuthSetup`

> This example assumes you've also handled logging in.
Isolates Apify authentication state per suite (or per test). Use it in any file that logs in, pushes, pulls, or otherwise touches `~/.apify`. By default it recreates the auth setup per test; pass `{ perTest: false }` to share one setup across the suite.

```typescript
import { useTempPath } from "./__setup__/hooks";
import { writeFile } from "node:fs/promises";
import { useAuthSetup } from "./__setup__/hooks";

const ACTOR_NAME = "owo";
useAuthSetup();
// or: useAuthSetup({ perTest: false });
```

API-dependent test cases must have `[api]` in the test name and live in `test/api/`. Files outside `test/api/` may mix local and `[api]` tests — the `test:local` script skips the `[api]` ones by name.

### `useTempPath`

Creates (and cleans up) a temporary directory, and optionally mocks `process.cwd()` so commands run as if executed there.

```typescript
import { useTempPath } from "./__setup__/hooks";

const {
beforeAllCalls,
afterAllCalls,
joinPath,
toggleCwdBetweenFullAndParentPath,
} = useTempPath(ACTOR_NAME, {
} = useTempPath("my-actor", {
cwd: true,
cwdParent: true,
create: true,
remove: true,
});

const { CreateCommand } = await import("../../src/commands/create.js");
const { PushCommand } = await import("../../src/commands/push.js");
const { CallCommand } = await import("../../src/commands/call.js");
```

beforeAll(async () => {
await beforeAllCalls();
Options:

await CreateCommand.run(
[ACTOR_NAME, "--template", "project_empty", "--skip-dependency-install"],
import.meta.url,
);
- `create` (default `true`) — create the temp directory in `beforeAll`.
- `remove` (default `true`) — remove it in `afterAll`.
- `cwd` (default `false`) — mock `process.cwd()` to point at the temp directory.
- `cwdParent` (default `false`) — start the mocked cwd at the parent of the temp directory.

const code = `
import { Actor } from 'apify';
Returned helpers:

Actor.main(async () => {
await Actor.setValue('OUTPUT', 'Hello world!');
console.log('Done!');
});`;
- `tmpPath` — absolute path of the temp directory.
- `joinPath(...segments)` — `path.join` rooted at `tmpPath`.
- `beforeAllCalls`, `afterAllCalls` — call these from your `beforeAll` / `afterAll`.
- `toggleCwdBetweenFullAndParentPath()` — flip the mocked cwd between the temp dir and its parent.

await writeFile(joinPath("main.js"), code);
**Important when using `cwd: true`:** in the code you are testing, always `import process from 'node:process'` (never `globalThis.process`), and dynamically `await import(...)` anything that reads cwd **after** `useTempPath` runs.

toggleCwdBetweenFullAndParentPath();
### Running individual commands

await PushCommand.run(["--no-prompt"], import.meta.url);
});
Use `testRunCommand` from the command framework to invoke CLI commands in tests — it bypasses the entry-point wrapper and gives you the parsed context directly.

afterAll(async () => {
await afterAllCalls();
});
```
## Pull requests

- Branch from `master`. Name branches descriptively (e.g. `fix/push-handles-empty-dir`, `feat/actors-search`).
- Write [Conventional Commit](https://www.conventionalcommits.org/) messages: `feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`. The changelog generator (`git-cliff`) groups entries by prefix, so this matters.
- Keep PRs focused. Unrelated cleanup is easier to review in a separate PR.
- Include tests when you fix a bug or add behavior.
- Update `docs/` via `yarn update-docs` if you changed any command.

Open PRs against `master`. Code owners are configured in [`.github/CODEOWNERS`](./.github/CODEOWNERS) and will be requested automatically.

## Releases

Releases are fully automated via GitHub Actions — **do not bump the version in `package.json` manually**.

### Running commands
- **Pre-releases (beta):** every push to `master` triggers `.github/workflows/pre_release.yaml`, which computes the next beta version, updates `CHANGELOG.md`, and publishes to npm under the `beta` tag.
- **Stable releases:** trigger the **Create a release** workflow (`.github/workflows/release.yaml`) manually from the GitHub Actions UI. It computes the next version (auto / patch / minor / major / custom), updates `CHANGELOG.md`, builds standalone bundles for Linux / macOS / Windows (x64 + ARM64), creates a GitHub release with the bundles attached, publishes to npm under `latest`, and opens a PR against the Homebrew formula.

Running commands in tests can be done by calling `testRunCommand` (imported from the command framework)
Only users with publish access to the [`apify-cli` npm package](https://www.npmjs.com/package/apify-cli) can trigger the stable release workflow.
Loading