# Fluent CI CD

> This skill should be used when the user asks to "set up ci for fluent", "github actions for servicenow", "deploy fluent from a pipeline", "fluent pull request checks", "automate now-sdk", or reports "keys.ts out of date" — anything about running the ServiceNow SDK (now-sdk) headless in CI/CD.

- Skill: `serac-labs/fluent-ci-cd` (Agent Skill)
- Install (CLI): `npx skillmds@latest add serac-labs/fluent-ci-cd`
- Raw SKILL.md: https://api.skillmd.com/api/skills/serac-labs/fluent-ci-cd/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: Apache-2.0
- Author: serac-labs (https://skillmd.com/u/serac-labs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/serac-labs/fluent-ci-cd

---


# Fluent CI/CD Pipelines

How to run a ServiceNow Fluent (SDK 4.x, `now-sdk` CLI) project through version control and CI: what to commit, the PR check that catches stale `keys.ts`, headless auth, a minimal GitHub Actions workflow, and the hard line between CI deploys and production promotion. For the official reference, run `snow_fluent_explain` with topic `ci-integration` (offline `now-sdk explain ci-integration`).

> **Where these tools run:** the `snow_fluent_*` tools are local-only — they execute the ServiceNow SDK (`now-sdk`) on the developer's machine, so they only exist when the MCP server runs locally over stdio. Over a hosted HTTP transport (e.g. a web chat) they are not available, so do not call them; drive the same flow through git + CI instead: commit the project files, trigger the build/deploy pipeline, and follow the workflow runs.


## 1. Repo discipline

The `.gitignore` that `snow_fluent_init` scaffolds is already correct — keep it as-is:

```gitignore
.DS_Store
.now/
dist/
node_modules/
target/
*.tsbuildinfo
.jest_cache
```

`dist/` and `.now/` are build outputs; never commit them.

**`src/fluent/generated/keys.ts` MUST be committed.** It is the registry mapping every `Now.ID['...']` to a sys_id — the record-identity source of truth. It is regenerated by every build. If a teammate's CI or machine builds without your committed keys, records get NEW sys_ids and `install` creates duplicates on the instance.

```text
❌ Add keys.ts to .gitignore ("it's generated, so ignore it")
   → duplicate records on the next install from another machine
✅ Commit keys.ts on every change, enforce with --frozenKeys in PR checks (section 2)
```

One app per repo is the simplest layout and what these examples assume. The SDK itself doesn't mandate a layout — projects can be arranged freely as long as `now.config.json` is set up correctly. `now.config.json` binds the project to one scope (`scope`/`scopeId`/`name`; optionally `tsconfigPath`) and contains no instance connection info, so it is safe to commit.

## 2. The PR check: build with frozen keys

Every pull request should run:

```bash
npm ci
npx now-sdk build --frozenKeys
```

`--frozenKeys` makes the build FAIL if compiling the Fluent code would change `keys.ts`. The exact failure message:

```text
Keys file is out-of-date. To update it, run the build again without frozen keys.
```

What that failure means: a developer added or renamed a `Now.ID` and forgot to commit the regenerated `keys.ts`. The fix is on the PR author's machine, not in CI:

```bash
now-sdk build      # regenerates keys.ts
git add src/fluent/generated/keys.ts && git commit
```

Locally, the same gate is `snow_fluent_build` with `frozen_keys=true`. Add `error_on_conflict=true` (`--errorOnConflict`) on brownfield projects so a record that exists both as Fluent code and as XML in `metadata/` fails the build instead of silently shipping the XML version.

**ES5 note:** Fluent DSL files (`*.now.ts`) are modern TypeScript — `const`, arrow functions, template literals are all correct there, and CI compiles them with the SDK's own toolchain. The ES5-only rule applies ONLY to script content that executes on the instance's Rhino engine, i.e. the string you put in a `script:` property (Business Rule bodies, Script Includes, etc.). A `--frozenKeys` build will not catch ES6 inside a `script:` string; review those by hand or with the `code-review` skill.

## 3. Headless auth (env vars, no keychain)

`now-sdk auth --add` is interactive (inquirer prompts, no `--username`/`--password` flags) — it cannot run in CI. Instead set env vars, which "take precedence over stored credentials" and bypass the OS keychain entirely:

```bash
# Always required to enable CI mode:
SN_SDK_NODE_ENV=SN_SDK_CI_INSTALL

# Option A — basic auth (default if SN_SDK_AUTH_TYPE unset):
SN_SDK_AUTH_TYPE=basic
SN_SDK_INSTANCE_URL=https://yourtest.service-now.com
SN_SDK_USER=ci.user
SN_SDK_USER_PWD=********

# Option B — OAuth client_credentials (SDK 4.7.0+, token fetched per run from ${SN_SDK_INSTANCE_URL}/oauth_token.do):
SN_SDK_AUTH_TYPE=oauth
SN_SDK_INSTANCE_URL=https://yourtest.service-now.com
SN_SDK_OAUTH_CLIENT_ID=...
SN_SDK_OAUTH_CLIENT_SECRET=...
```

Instance-side prerequisites for Option B (do these once on the target instance):

1. Set sys_property `glide.oauth.inbound.client.credential.grant_type.enabled` = `true`.
2. Application Registry entry of type "OAuth API endpoint for external clients", Public Client = false, Grant type includes Client Credentials.
3. Map an OAuth Application User that has install roles (typically admin) AND **Identity Type = Human** — Machine identities are blocked during the SDK's session-token handshake.

```text
❌ Run `now-sdk auth --add` in a CI step          → hangs on interactive prompts
❌ OAuth app user with Identity Type = Machine     → install fails at the CSRF/session step
✅ SN_SDK_NODE_ENV=SN_SDK_CI_INSTALL + SN_SDK_* secrets from the CI secret store
```

## 4. Minimal GitHub Actions workflow

There is no official GitHub Action for the Fluent SDK (the marketplace "ServiceNow CI/CD" actions are the classic App-Repo flow). Official guidance is plain `now-sdk` steps. Node 20+ is required (`engines: node >=20.18.0`).

```yaml
name: fluent-ci
on:
  pull_request:
  push:
    branches: [main]

jobs:
  verify:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - name: Verify keys.ts is up to date
        run: npx now-sdk build --frozenKeys

  deploy-test:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    env:
      SN_SDK_NODE_ENV: SN_SDK_CI_INSTALL
      SN_SDK_AUTH_TYPE: oauth
      SN_SDK_INSTANCE_URL: ${{ secrets.SN_TEST_INSTANCE_URL }}
      SN_SDK_OAUTH_CLIENT_ID: ${{ secrets.SN_TEST_OAUTH_CLIENT_ID }}
      SN_SDK_OAUTH_CLIENT_SECRET: ${{ secrets.SN_TEST_OAUTH_CLIENT_SECRET }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx now-sdk build --frozenKeys
      - name: Install to TEST instance
        run: npx now-sdk install
```

Tuning the install step: `--demoData` defaults to TRUE (pass nothing to keep it; relevant when your app ships demo records), `--skip-flow-activation` disables the automatic flow publish that `install` does since SDK 4.5.

## 5. Production: NEVER install from CI

Official SDK guidance, verbatim: **"Do not use `now-sdk install` from CI to deploy to production instances."** The promotion path is git plus the Application Repository: publish the app from the test instance to the App Repo and install it in production from there — that is where change management and rollback live.

Why this is a hard rule: `now-sdk install` bypasses update sets ("Installing via now-sdk does not generate changes in update sets as of now") and "there is no rollback context created for app installed via now-sdk". The only SDK-side undo is `--reinstall` (uninstall + fresh install — destructive).

```text
❌ main-branch job that runs now-sdk install against prod
✅ CI installs to a shared TEST instance → publish from that instance to the
   Application Repository → install in test/prod via the App Repo (change
   management + rollback live there)
```

## 6. Exit-code and output gotchas

- Fluent compile errors → exit 1 with `ERROR: Build failed due to errors`. Good, fails the job.
- **Missing/unfindable `package.json` → `[now-sdk] ERROR: Could not find package.json...` but exit code 0.** A misconfigured `working-directory` makes the build step "pass" while building nothing. Guard it:

```yaml
      - name: Build
        run: |
          npx now-sdk build --frozenKeys
          test -d dist/app   # fail the job if nothing was actually built
```

- There is no JSON output mode anywhere in the CLI; output is human log lines prefixed `[now-sdk]`. Grep stdout/stderr if you need to assert on messages.
- Locally, mirror the pipeline before pushing: `snow_fluent_status` to confirm scope/SDK pin/keys.ts state, `snow_fluent_build` with `frozen_keys=true` for the PR gate, `snow_fluent_install` for the test-instance deploy.

