# sf-package-list

[![NPM](https://img.shields.io/npm/v/sf-package-list.svg?label=sf-package-list)](https://www.npmjs.com/package/sf-package-list)
[![Downloads/week](https://img.shields.io/npm/dw/sf-package-list.svg)](https://npmjs.org/package/sf-package-list)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://raw.githubusercontent.com/mcarvin8/sf-package-list/main/LICENSE.md)
[![Maintainability](https://qlty.sh/badges/3f1779cc-038e-48f0-b693-52f72e106d67/maintainability.svg)](https://qlty.sh/gh/mcarvin8/projects/sf-package-list)
[![codecov](https://codecov.io/gh/mcarvin8/sf-package-list/graph/badge.svg?token=SAT4HZCEHU)](https://codecov.io/gh/mcarvin8/sf-package-list)
[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fmcarvin8%2Fsf-package-list%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/mcarvin8/sf-package-list/main)

Convert Salesforce `package.xml` manifests to and from a human-readable list format. Available as a **Salesforce CLI plugin** for any provider, and as a **native GitHub Action** for GitHub Actions users who want to skip installing the CLI.

---

## Requirements

- Salesforce CLI (`sf`)
- Node.js **22.19 or later**

---

## Install

```bash
sf plugins install sf-package-list
```

---

## GitHub Action

For GitHub Actions, this is also available as a native Action — no `sf` CLI or plugin install required. One action, two `mode`s:

### `mode: to-list` — package.xml → list

```yaml
- name: Convert package.xml to a list
  id: to-list
  uses: mcarvin8/sf-package-list@v3
  with:
    mode: to-list
    package-xml: package.xml
    package-list: package.txt
```

### `mode: to-xml` — list → package.xml

```yaml
- name: Convert a list to package.xml
  id: to-xml
  uses: mcarvin8/sf-package-list@v3
  with:
    mode: to-xml
    package-list: package.txt
    package-xml: package.xml
```

### Inputs

| Input             | Description                                                                                                     | Required | Default        |
| ------------------ | ------------------------------------------------------------------------------------------------------------------- | -------- | -------------- |
| `mode`               | Conversion direction: `to-list` (package.xml → list) or `to-xml` (list → package.xml).                             | **Yes**  |                 |
| `package-xml`        | `to-list`: source `package.xml`. `to-xml`: output path for the generated `package.xml`.                            | No       | `package.xml`   |
| `package-list`       | `to-list`: output path for the generated package list. `to-xml`: source package list.                              | No       | `package.txt`   |
| `no-api-version`     | Explicitly omit the API version from the generated output.                                                        | No       | `false`         |
| `fail-on-empty`      | Fail the action if the generated output has no types.                                                             | No       | `false`         |

### Outputs

| Output               | Description                                                                       |
| --------------------- | ------------------------------------------------------------------------------------- |
| `package-list-path`   | Path to the generated package list (`to-list` mode only).                          |
| `package-xml-path`    | Path to the generated `package.xml` (`to-xml` mode only).                          |
| `types`               | Number of distinct metadata types processed.                                       |
| `members`             | Number of members processed.                                                       |
| `api-version`         | API version used in the generated `package.xml` (`to-xml` mode only; empty when omitted). |
| `warnings`            | Newline-separated list of warnings emitted while converting, if any.               |

---

## List Format

Each metadata type gets one line: `TypeName: member1, member2, ...`

```
CustomLabel: Always_Be_Closing, Attention_Interest_Decision_Action, Leads_Are_Gold
CustomObject: ABC, Glengarry, Mitch_And_Murray
CustomField: Glengarry.Weak_Leadz__c, Coffee.is_Closer__c
EmailTemplate: unfiled$public/Second_Prize_Set_of_Steak_Knives
StandardValueSet: Glengarry_Leads, Cadillac_Eldorado
Version: 59.0
```

<details>
<summary>Equivalent package.xml</summary>

```xml
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <members>Always_Be_Closing</members>
        <members>Attention_Interest_Decision_Action</members>
        <members>Leads_Are_Gold</members>
        <name>CustomLabel</name>
    </types>
    <types>
        <members>ABC</members>
        <members>Glengarry</members>
        <members>Mitch_And_Murray</members>
        <name>CustomObject</name>
    </types>
    <types>
        <members>Glengarry.Weak_Leadz__c</members>
        <members>Coffee.is_Closer__c</members>
        <name>CustomField</name>
    </types>
    <types>
        <members>unfiled$public/Second_Prize_Set_of_Steak_Knives</members>
        <name>EmailTemplate</name>
    </types>
    <types>
        <members>Glengarry_Leads</members>
        <members>Cadillac_Eldorado</members>
        <name>StandardValueSet</name>
    </types>
    <version>59.0</version>
</Package>
```

</details>

---

## Commands

### `sf sfpl list`

> package.xml → list

```bash
sf sfpl list -x <package.xml> [-l <output.txt>] [-n]
```

| Flag               | Short | Default       | Description                      |
| ------------------ | ----- | ------------- | -------------------------------- |
| `--package-xml`    | `-x`  | —             | Path to the source `package.xml` |
| `--package-list`   | `-l`  | `package.txt` | Output path for the list file    |
| `--no-api-version` | `-n`  | `false`       | Omit API version from output     |

### `sf sfpl xml`

> list → package.xml

```bash
sf sfpl xml -l <list.txt> [-x <package.xml>] [-n]
```

| Flag               | Short | Default       | Description                                 |
| ------------------ | ----- | ------------- | ------------------------------------------- |
| `--package-list`   | `-l`  | —             | Path to the source list file                |
| `--package-xml`    | `-x`  | `package.xml` | Output path for the generated `package.xml` |
| `--no-api-version` | `-n`  | `false`       | Omit API version from output                |

---

## Use Cases

### CI/CD Pipelines

The list format is easier to diff, review, and edit than XML, making it well suited for pull requests, merge requests, deployment approvals, and release workflows.

Because metadata is represented in a concise text format, pipeline logs can clearly show exactly what will be deployed before a deployment step executes.

Example deployment manifest:

```text
CustomObject: Invoice__c
CustomField: Invoice__c.Status__c, Invoice__c.Amount__c
ApexClass: InvoiceController
```

This provides a human-readable deployment summary without requiring reviewers to inspect generated XML.

**Example: print the `sfdx-git-delta` package.xml as a list before deploying**

[`sfdx-git-delta`](https://github.com/scolladon/sfdx-git-delta) generates a `package.xml`/`destructiveChanges.xml` from a git diff. Convert the generated `package.xml` to list format and print it to the job log so reviewers can see exactly what will deploy before the deploy step runs:

```yaml
name: deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'

      - name: Install Salesforce CLI
        run: npm install -g @salesforce/cli@latest

      - name: Install plugins
        run: |
          echo y | sf plugins install sfdx-git-delta
          echo y | sf plugins install sf-package-list

      - name: Generate delta package.xml
        run: sf sgd source delta --to HEAD --from HEAD^ --output .

      - name: Print deployment manifest as list
        run: sf sfpl list -x package/package.xml

      - name: Authenticate to Salesforce
        run: sf org login sfdx-url --sfdx-url-file ${{ secrets.SFDX_AUTH_URL }} --alias ci-org

      - name: Deploy delta
        run: sf project deploy start -x package/package.xml -o ci-org
```

The `package.txt` printed in the log becomes a quick, reviewable manifest of the deployment—no need to open the generated XML to confirm scope.

### Controlled Destructive Deployments

Use a package list as a restricted allowlist of metadata to remove.

Teams can maintain destructive changes in source control as a simple text file, review them during change approval, convert them to `destructiveChanges.xml` during pipeline execution, and deploy only the explicitly declared components.

This approach helps reduce the risk of accidentally deleting metadata that was not intended to be included in a destructive deployment.

**Example: `workflow_dispatch` input (package list) → `destructiveChanges.xml`**

Accept the list of components to delete as a manual `workflow_dispatch` text input, restricting the destructive deployment to exactly what an approver typed in—no free-form XML editing required:

```yaml
name: destructive-deploy

on:
  workflow_dispatch:
    inputs:
      destructive_list:
        description: 'Components to delete, in package-list format (e.g. "CustomField: Invoice__c.Old_Field__c")'
        required: true
        type: string

jobs:
  destroy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'

      - name: Install Salesforce CLI
        run: npm install -g @salesforce/cli@latest

      - name: Install plugin
        run: echo y | sf plugins install sf-package-list

      - name: Write approved list to file
        run: printf '%s\n' "${{ github.event.inputs.destructive_list }}" > destructive.txt

      - name: Convert list to destructiveChanges.xml
        run: sf sfpl xml -l destructive.txt -x destructiveChanges.xml -n

      - name: Write empty package.xml
        run: sf sfpl xml -x package.xml -n

      - name: Authenticate to Salesforce
        run: sf org login sfdx-url --sfdx-url-file ${{ secrets.SFDX_AUTH_URL }} --alias ci-org

      - name: Deploy destructive changes
        run: sf project deploy start -x package.xml --post-destructive-changes destructiveChanges.xml -o ci-org
```

Because the destructive scope comes only from the typed `workflow_dispatch` input, the deployment is limited to exactly the components an approver declared—nothing picked up implicitly from a branch diff or working directory.

### Pipeline-Driven Metadata Selection

Store metadata selections in CI variables, deployment parameters, release manifests, or generated artifacts.

The list format is compact enough to be:

- Generated by automation
- Passed between pipeline stages
- Embedded in release tickets
- Stored as deployment manifests alongside source code

and then converted to `package.xml` only when needed.

### Release Management and Change Review

Use package lists as lightweight deployment manifests that can be reviewed by administrators, release managers, and auditors.

The format is intentionally simple enough for non-developers to verify:

```text
CustomLabel: Feature_Flag_X
PermissionSet: Sales_User
Flow: Lead_Routing
```

without needing Salesforce metadata XML knowledge.

### Metadata-as-Data Automation

Generate deployment manifests programmatically from:

- Change requests
- Ticketing systems
- Environment promotion tools
- Custom release automation
- Internal deployment portals

The resulting list can be transformed into a valid `package.xml` immediately before deployment, allowing automation to work with a simple text representation instead of XML.

### Documentation and Communication

Package lists are easy to paste into:

- Pull requests
- Change requests
- Release notes
- Deployment approvals
- Team chat discussions

This makes it easier to communicate deployment scope than sharing raw `package.xml` files.

---

## Troubleshooting

Manifests and list files are parsed in-house and checked only against *structure*, not against Salesforce's metadata registry. `<name>`/type-name values are taken as-is; a misspelled or nonexistent metadata type will pass through to the output instead of being rejected here—deployment will fail on it later, not here.

**Invalid `package.xml`** — A `<Package>` document that doesn't match the expected Metadata API manifest structure (multiple `<version>`, malformed `<types>`, unexpected elements, etc.) or has no `<types>` surfaces as a warning and produces empty output. The underlying parse error is appended to the warning.

**Invalid list lines** — Each malformed line is skipped with a warning. Valid lines still produce output.

The plugin never throws on bad input—it warns and continues.

---

## Issues

Found a bug or have a feature request? [Open an issue](https://github.com/mcarvin8/sf-package-list/issues).

---

## License

[MIT](LICENSE.md)
