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
41 changes: 26 additions & 15 deletions docs/design/0013-flow-and-permission-set-assignment-cleanup.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,21 @@ Resolution:
```sh
sf simply flow version prune --target-org my-org --source-dir sfdx-source/core
sf simply flow version prune --target-org my-org --source-dir sfdx-source/core --dry-run
sf simply flow version prune --target-org my-org --flow-name My_Flow --flow-name Another_Flow
```

| Flag | Char | Required | Purpose |
| -------------- | ---- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--target-org` | `-o` | Yes | |
| `--source-dir` | `-d` | Yes | One or more directories to glob `**/*.flow-meta.xml` under — replaces the original's implicit glob from `cwd`, matching every other command in this repo taking an explicit source scope rather than assuming the working directory. |
| `--dry-run` | | No | List what would be deleted without deleting anything. Default `false`. New relative to the original, which had no preview before deleting org-wide flow-version history. |
| Flag | Char | Required | Purpose |
| -------------- | ---- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--target-org` | `-o` | Yes | |
| `--source-dir` | `-d` | One of\* | One or more directories to glob `**/*.flow-meta.xml` under — replaces the original's implicit glob from `cwd`, matching every other command in this repo taking an explicit source scope rather than assuming the working directory. |
| `--flow-name` | `-n` | One of\* | Explicit Flow `DeveloperName`(s), repeatable — an alternative to `--source-dir` for scripted or one-off use, matching `flow delete`'s `--flow-name`. Added after the initial release, when it became clear a caller who already knows the flow name shouldn't need a local checkout just to prune it. |
| `--dry-run` | | No | List what would be deleted without deleting anything. Default `false`. New relative to the original, which had no preview before deleting org-wide flow-version history. |

\* Exactly one of `--source-dir`/`--flow-name` — same XOR shape as `flow delete`'s `--manifest`/`--flow-name`.

Resolution:

1. Glob `**/*.flow-meta.xml` under each `--source-dir`; derive Flow developer names from file basenames.
1. Resolve the flow developer names: glob `**/*.flow-meta.xml` under each `--source-dir` and derive names from file basenames, or `--flow-name` directly.
2. Tooling API, chunked: `SELECT Id, Definition.DeveloperName FROM Flow WHERE Status = 'Obsolete' AND Definition.DeveloperName IN (...)`.
3. `--dry-run`: print/return the candidate list, delete nothing.
4. Otherwise: delete each version (failure-collection, not throw-on-first, same as `flow delete`), print a summary, `process.exitCode = 1` on any failure.
Expand Down Expand Up @@ -151,15 +155,16 @@ Parses a `<Package><types><name>...</name><members>...</members></types>...</Pac

**Unit/command** (`simply-flow`, `simply-permissions`), mocked `Connection`:

| Case | What it pins down |
| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `flow delete --file` with no `Flow` members in the file | No-op, `info.nothingToDelete`, no queries issued. |
| `flow delete`: one deactivation fails, others succeed | Failure recorded in `failures`, remaining flows still processed; `process.exitCode === 1`. |
| `flow version prune --dry-run` | Candidate list returned/printed; no delete call made. |
| `assignment delete --file` with both `PermissionSet` and `PermissionSetGroup` members | Both queries run, results unioned, deleted in one chunked pass. |
| `assignment delete`: more than 200 matching assignments | Deletes in multiple 200-record chunks. |
| `--file` and an explicit name flag both given (any of the three commands) | Rejected — mutually exclusive input sources. |
| Every query's `IN (...)` clause | Built via `chunkedInQuery`/`escapeSoqlLiteral` — a name containing `'` doesn't break the query. |
| Case | What it pins down |
| ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `flow delete --file` with no `Flow` members in the file | No-op, `info.nothingToDelete`, no queries issued. |
| `flow delete`: one deactivation fails, others succeed | Failure recorded in `failures`, remaining flows still processed; `process.exitCode === 1`. |
| `flow version prune --dry-run` | Candidate list returned/printed; no delete call made. |
| `flow version prune --flow-name` | Pruned without a `--source-dir` scan; the flag's names are used directly in the query. |
| `assignment delete --file` with both `PermissionSet` and `PermissionSetGroup` members | Both queries run, results unioned, deleted in one chunked pass. |
| `assignment delete`: more than 200 matching assignments | Deletes in multiple 200-record chunks. |
| `--file`/`--source-dir` and an explicit name flag both given (any of the three commands) | Rejected — mutually exclusive input sources. |
| Every query's `IN (...)` clause | Built via `chunkedInQuery`/`escapeSoqlLiteral` — a name containing `'` doesn't break the query. |

**NUT** — none proposed initially, matching most commands in this repo; revisit if a real destructive-deploy scenario needs end-to-end coverage against a scratch org.

Expand Down Expand Up @@ -201,3 +206,9 @@ A few places where implementing this taught something the design above didn't an
than the generic `--file` this doc originally proposed. `permissions assignment delete` was left on
`--file`/`-f` — revisit for the same rename if it turns out inconsistency between the two sibling
commands is confusing in practice.
- **`flow version prune` gained a `--flow-name` flag** after implementation, making `--source-dir`
optional and adding the same one-of-two XOR shape `flow delete` already has between its manifest/file
input and explicit names. Motivation: pruning obsolete versions for a flow you already know the name
of shouldn't require a local checkout just to glob a `*.flow-meta.xml` file that only exists to
supply the name the command already has another way to get. No new query shape was needed — the
resolved `flowNames` list feeds the same `chunkedInQuery` call regardless of which flag supplied it.
18 changes: 11 additions & 7 deletions packages/simply-flow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,12 @@ Delete obsolete versions of Flows found in local source.

```
USAGE
$ sf simply flow version prune -o <value> -d <value>... [--json] [--flags-dir <value>] [--api-version <value>]
[--dry-run]
$ sf simply flow version prune -o <value> [--json] [--flags-dir <value>] [--api-version <value>] [-d <value>...] [-n
<value>...] [--dry-run]

FLAGS
-d, --source-dir=<value>... (required) Directories to scan for *.flow-meta.xml files
-d, --source-dir=<value>... Directories to scan for *.flow-meta.xml files
-n, --flow-name=<value>... Flow DeveloperName(s) to prune obsolete versions for
-o, --target-org=<value> (required) Username or alias of the target org. Not required if the `target-org`
configuration variable is already set.
--api-version=<value> Override the api version used for api requests made by this command
Expand All @@ -89,17 +90,20 @@ GLOBAL FLAGS
DESCRIPTION
Delete obsolete versions of Flows found in local source.

Scans one or more source directories for `*.flow-meta.xml` files, then deletes any Tooling API Flow version already
`Status = 'Obsolete'` for those flows — keeping an org's Flow version history from accumulating indefinitely. Unlike
`simply flow delete`, this never touches an active Flow; it only removes versions the org itself already marked
obsolete.
Flows can be named either via one or more `--source-dir` directories, scanned for `*.flow-meta.xml` files, or via one
or more `--flow-name` flags for scripted or one-off use. Exactly one of the two must be given. The command then
deletes any Tooling API Flow version already `Status = 'Obsolete'` for those flows — keeping an org's Flow version
history from accumulating indefinitely. Unlike `simply flow delete`, this never touches an active Flow; it only
removes versions the org itself already marked obsolete.

Use `--dry-run` to see what would be deleted without deleting anything.

EXAMPLES
$ sf simply flow version prune --target-org myOrg --source-dir sfdx-source/core

$ sf simply flow version prune --target-org myOrg --source-dir sfdx-source/core --dry-run

$ sf simply flow version prune --target-org myOrg --flow-name My_Flow --flow-name Another_Flow
```

_See code: [lib/commands/simply/flow/version/prune.js](https://github.com/SimplySF/simply-node/blob/@simplysf/simply-flow@0.2.0/packages/simply-flow/lib/commands/simply/flow/version/prune.js)_
Expand Down
4 changes: 2 additions & 2 deletions packages/simply-flow/command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"alias": [],
"command": "simply:flow:version:prune",
"flagAliases": [],
"flagChars": ["d", "o"],
"flags": ["api-version", "dry-run", "flags-dir", "json", "source-dir", "target-org"],
"flagChars": ["d", "n", "o"],
"flags": ["api-version", "dry-run", "flags-dir", "flow-name", "json", "source-dir", "target-org"],
"plugin": "@simplysf/simply-flow"
}
]
12 changes: 11 additions & 1 deletion packages/simply-flow/messages/simply.flow.version.prune.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ Delete obsolete versions of Flows found in local source.

# description

Scans one or more source directories for `*.flow-meta.xml` files, then deletes any Tooling API Flow version already `Status = 'Obsolete'` for those flows — keeping an org's Flow version history from accumulating indefinitely. Unlike `simply flow delete`, this never touches an active Flow; it only removes versions the org itself already marked obsolete.
Flows can be named either via one or more `--source-dir` directories, scanned for `*.flow-meta.xml` files, or via one or more `--flow-name` flags for scripted or one-off use. Exactly one of the two must be given. The command then deletes any Tooling API Flow version already `Status = 'Obsolete'` for those flows — keeping an org's Flow version history from accumulating indefinitely. Unlike `simply flow delete`, this never touches an active Flow; it only removes versions the org itself already marked obsolete.

Use `--dry-run` to see what would be deleted without deleting anything.

# flags.source-dir.summary

Directories to scan for *.flow-meta.xml files

# flags.flow-name.summary

Flow DeveloperName(s) to prune obsolete versions for

# flags.dry-run.summary

List obsolete versions without deleting them
Expand All @@ -22,6 +26,12 @@ List obsolete versions without deleting them

- <%= config.bin %> <%= command.id %> --target-org myOrg --source-dir sfdx-source/core --dry-run

- <%= config.bin %> <%= command.id %> --target-org myOrg --flow-name My_Flow --flow-name Another_Flow

# error.sourceDirOrFlowNameRequired

You must specify either --source-dir or --flow-name, but not both.

# info.scanningLocalSource

Scanning local source for Flows...
Expand Down
36 changes: 25 additions & 11 deletions packages/simply-flow/src/commands/simply/flow/version/prune.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ function developerNameFromFlowFile(flowFile: string): string {

/**
* Deletes obsolete Flow versions (`Status = 'Obsolete'`) for the Flows found under `--source-dir`,
* to keep an org's Flow version history from accumulating indefinitely. Unlike `flow delete`, this
* never removes an active Flow — only versions the org itself already marked obsolete.
* or for explicitly named Flows via `--flow-name`. Unlike `flow delete`, this never removes an
* active Flow — only versions the org itself already marked obsolete.
*/
export default class FlowVersionPrune extends SfCommand<FlowVersionPruneResult> {
public static readonly summary = messages.getMessage('summary');
Expand All @@ -72,7 +72,11 @@ export default class FlowVersionPrune extends SfCommand<FlowVersionPruneResult>
char: 'd',
exists: true,
multiple: true,
required: true,
}),
'flow-name': Flags.string({
summary: messages.getMessage('flags.flow-name.summary'),
char: 'n',
multiple: true,
}),
'dry-run': Flags.boolean({ summary: messages.getMessage('flags.dry-run.summary'), default: false }),
};
Expand All @@ -81,14 +85,24 @@ export default class FlowVersionPrune extends SfCommand<FlowVersionPruneResult>
public async run(): Promise<FlowVersionPruneResult> {
const { flags } = await this.parse(FlowVersionPrune);

this.spinner.start(messages.getMessage('info.scanningLocalSource'));
const flowFiles = (
await Promise.all(
flags['source-dir'].map((sourceDir) => glob(`${sourceDir.replaceAll('\\', '/')}/**/*.flow-meta.xml`)),
)
).flat();
const flowNames = [...new Set(flowFiles.map(developerNameFromFlowFile))];
this.spinner.stop();
const hasSourceDir = Boolean(flags['source-dir']?.length);
const hasFlowNames = Boolean(flags['flow-name']?.length);
if ((hasSourceDir && hasFlowNames) || (!hasSourceDir && !hasFlowNames)) {
throw messages.createError('error.sourceDirOrFlowNameRequired');
}

let flowNames: string[];
if (hasSourceDir) {
this.spinner.start(messages.getMessage('info.scanningLocalSource'));
const sourceDirs = flags['source-dir'] as string[];
const flowFiles = (
await Promise.all(sourceDirs.map((sourceDir) => glob(`${sourceDir.replaceAll('\\', '/')}/**/*.flow-meta.xml`)))
).flat();
flowNames = [...new Set(flowFiles.map(developerNameFromFlowFile))];
this.spinner.stop();
} else {
flowNames = flags['flow-name'] as string[];
}

const connection = requireConnection(flags);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,34 @@ describe('simply flow version prune', () => {
expect(result.failures).to.deep.equal([]);
expect(process.exitCode).to.equal(undefined);
});

it('prunes obsolete versions for explicitly named flows via --flow-name, without scanning source', async () => {
const autoFetchQuery = $$.SANDBOX.stub(Connection.prototype, 'autoFetchQuery').resolves({
records: [{ Id: '301000000000001AAA', Definition: { DeveloperName: 'My_Flow' } }],
done: true,
totalSize: 1,
} as never);

const result = await FlowVersionPrune.run([
'--target-org',
testOrg.username,
'--flow-name',
'My_Flow',
'--dry-run',
]);

expect(result.dryRun).to.be.true;
expect(result.candidates).to.deep.equal([{ id: '301000000000001AAA', developerName: 'My_Flow' }]);
expect(autoFetchQuery.getCall(0).args[0]).to.include("Definition.DeveloperName IN ('My_Flow')");
});

it('rejects --source-dir combined with --flow-name', async () => {
await expect(
FlowVersionPrune.run(['--target-org', testOrg.username, '--source-dir', tmpDir, '--flow-name', 'My_Flow']),
).rejects.toThrow();
});

it('rejects when neither --source-dir nor --flow-name is given', async () => {
await expect(FlowVersionPrune.run(['--target-org', testOrg.username])).rejects.toThrow();
});
});
18 changes: 11 additions & 7 deletions packages/simply/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1037,11 +1037,12 @@ Delete obsolete versions of Flows found in local source.

```
USAGE
$ sf simply flow version prune -o <value> -d <value>... [--json] [--flags-dir <value>] [--api-version <value>]
[--dry-run]
$ sf simply flow version prune -o <value> [--json] [--flags-dir <value>] [--api-version <value>] [-d <value>...] [-n
<value>...] [--dry-run]

FLAGS
-d, --source-dir=<value>... (required) Directories to scan for *.flow-meta.xml files
-d, --source-dir=<value>... Directories to scan for *.flow-meta.xml files
-n, --flow-name=<value>... Flow DeveloperName(s) to prune obsolete versions for
-o, --target-org=<value> (required) Username or alias of the target org. Not required if the `target-org`
configuration variable is already set.
--api-version=<value> Override the api version used for api requests made by this command
Expand All @@ -1054,17 +1055,20 @@ GLOBAL FLAGS
DESCRIPTION
Delete obsolete versions of Flows found in local source.

Scans one or more source directories for `*.flow-meta.xml` files, then deletes any Tooling API Flow version already
`Status = 'Obsolete'` for those flows — keeping an org's Flow version history from accumulating indefinitely. Unlike
`simply flow delete`, this never touches an active Flow; it only removes versions the org itself already marked
obsolete.
Flows can be named either via one or more `--source-dir` directories, scanned for `*.flow-meta.xml` files, or via one
or more `--flow-name` flags for scripted or one-off use. Exactly one of the two must be given. The command then
deletes any Tooling API Flow version already `Status = 'Obsolete'` for those flows — keeping an org's Flow version
history from accumulating indefinitely. Unlike `simply flow delete`, this never touches an active Flow; it only
removes versions the org itself already marked obsolete.

Use `--dry-run` to see what would be deleted without deleting anything.

EXAMPLES
$ sf simply flow version prune --target-org myOrg --source-dir sfdx-source/core

$ sf simply flow version prune --target-org myOrg --source-dir sfdx-source/core --dry-run

$ sf simply flow version prune --target-org myOrg --flow-name My_Flow --flow-name Another_Flow
```

_See code: [@simplysf/simply-flow](https://github.com/SimplySF/simply-node/blob/@simplysf/simply-flow@0.2.0/packages/simply-flow/lib/commands/simply/flow/version/prune.js)_
Expand Down
Loading