> ## Documentation Index
> Fetch the complete documentation index at: https://backstage.spotify.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Fleetshift: Writing and Publishing npm Package Shifts

> Write a deterministic shift as an npm package, publish it to your Portal registry, and run it against target repositories.

Fleetshift's npm-package shift type lets you write a deterministic shift as a standard
npm package. You publish a Node.js CLI to your
Portal instance's private registry and select it in the fleetshift wizard.

This is useful when you want the shift logic to live in your own repository, go through
your normal code review process, and be pinned to an exact version. Each change to the
shift is a reviewed PR, and each deployment is an `npm publish`.

## Writing a shift package

A shift package is a regular npm package with a `bin` entry. When fleetshift runs your
shift against a target repository, it:

1. Clones the target repository into a working directory.
2. Sets `process.cwd()` to the cloned repo.
3. Runs your binary.

Your binary modifies files in the working directory and exits. Fleetshift handles the git
diff, branch creation, and pull request.

### Minimal example

Directory structure:

```
my-shift/
├── package.json
└── bin/
    └── my-shift
```

**package.json:**

```json theme={"theme":{"light":"github-light","dark":"dracula"}}
{
  "name": "@fleetshift/my-shift",
  "version": "1.0.0",
  "bin": "./bin/my-shift",
  "files": ["bin"],
  "license": "UNLICENSED"
}
```

**bin/my-shift:**

```js theme={"theme":{"light":"github-light","dark":"dracula"}}
#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

const configPath = path.join(process.cwd(), '.eslintrc.json');

if (!fs.existsSync(configPath)) {
  console.log('No .eslintrc.json found — skipping.');
  process.exit(0);
}

const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
config.rules = config.rules ?? {};
config.rules['no-console'] = 'warn';

fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
console.log('Added no-console rule to .eslintrc.json');
```

Make the binary executable: `chmod +x bin/my-shift`

### Package naming rules

* All packages must use the `@fleetshift/` scope: `@fleetshift/my-shift`
* The unscoped part must be lowercase and can only contain letters, digits, `.`, `_`, `-`
* No protocol or path specifiers (`github:`, `https://`, `file:`)

### Exit codes

| Exit code | Result                                                                                                                      |
| --------- | --------------------------------------------------------------------------------------------------------------------------- |
| `0`       | Success. If your binary made file changes, fleetshift creates a PR. If no files changed, the target is marked "no changes". |
| Non-zero  | The target is marked as failed.                                                                                             |

## Publishing to your Portal registry

Portal exposes an npm registry proxy that your shift packages are published through. This
works the same way as publishing custom plugins — you use your existing Backstage service
token to authenticate.

### Step 1: Configure npm authentication

Add an `.npmrc` file to your shift repository (or your home directory):

```
@fleetshift:registry=https://<your-portal-host>/api/fleetshift/registry/npm/
//<your-portal-host>/api/fleetshift/registry/npm/:_authToken=${PORTAL_SERVICE_TOKEN}
```

Replace `<your-portal-host>` with your Portal instance's hostname (for example,
`my-company.spotifyportal.com`). The first line tells npm to route `@fleetshift/*`
packages to your Portal registry. Without it, npm uses the default registry and
authentication will fail.

To create your publish token:

1. Generate a secure random token string (e.g. `openssl rand -base64 32`).
2. In your Portal instance, go to **App Settings** (`/admin/app-settings`).
3. Under **External Access**, click **Add Item**.
4. Set type `static`, subject `fleetshift-npm-registry`, access restriction
   plugin `fleetshift`, and paste your generated token.
5. Save. Use the same token value in your `.npmrc` above.

### Step 2: Publish

```bash theme={"theme":{"light":"github-light","dark":"dracula"}}
npm publish
```

No `--registry` flag is needed — the `.npmrc` scope line handles routing.

### Publishing from CI

For automated publishing, store the service token as a CI secret. Example with GitHub
Actions:

```yaml theme={"theme":{"light":"github-light","dark":"dracula"}}
- name: Publish shift package
  env:
    PORTAL_SERVICE_TOKEN: ${{ secrets.PORTAL_SERVICE_TOKEN }}
  run: |
    echo "@fleetshift:registry=https://<your-portal-host>/api/fleetshift/registry/npm/" >> .npmrc
    echo "//<your-portal-host>/api/fleetshift/registry/npm/:_authToken=${PORTAL_SERVICE_TOKEN}" >> .npmrc
    npm publish
```

## Creating a shift in the wizard

1. Open `Fleetshift` in your Portal sidebar.
2. Click `New Shift` and select `npm Package`.
3. Enter the package name (for example, `@fleetshift/my-shift`).
4. Pick a version to pin. Only exact versions are accepted — no ranges like `^1.0.0` or
   `~1.0.0`.
5. Optionally specify a custom bin name. Defaults to the unscoped package name (for
   example, `my-shift` for `@fleetshift/my-shift`).
6. Optionally add arguments to pass to the binary.
7. Configure targets, PR template, and other settings as usual.
8. Click `Create`.

## Updating a shift version

To roll out a new version of your shift logic:

1. Make your changes and bump the version in `package.json`.
2. Open a PR in your shift repository and get it reviewed.
3. Merge and publish the new version.
4. In Fleetshift, edit the shift and update the pinned version.

## Sandbox constraints

npm package shifts run inside a locked-down container. Keep these restrictions in mind when
writing your binary.

| Constraint           | Detail                                                                                                                                                             |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| No lifecycle scripts | `npm install` runs with `--ignore-scripts`. Your package and its dependencies cannot run postinstall, preinstall, or prepare scripts. Native addons will not work. |
| Restricted network   | Outbound network is restricted. Your binary should not depend on making HTTP requests to external services at runtime.                                             |
| No root              | The container runs as a non-root user.                                                                                                                             |
| Non-interactive      | No stdin. Your binary cannot prompt for user input.                                                                                                                |

### A note on dependencies

Since lifecycle scripts are disabled, stick to pure JavaScript or TypeScript dependencies.
