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
197 changes: 196 additions & 1 deletion .github/actions/create-release-pr/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33835,6 +33835,176 @@ function validateCreateReleasePrInputs(raw) {
}


/***/ }),

/***/ 7814:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {

"use strict";

Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.parseApexMembers = parseApexMembers;
exports.findRelevantTests = findRelevantTests;
exports.runFindTestsAction = runFindTestsAction;
/**
* Select the Apex test classes relevant to a delta package: naming-convention
* matches plus a reference scan of every test class in the source tree. Pure
* and filesystem-agnostic — file access goes through the injected reader.
*/
const result_1 = __nccwpck_require__(3860);
const errors_1 = __nccwpck_require__(272);
const nodeSourceFileReader_1 = __nccwpck_require__(2605);
const APEX_TYPES = new Set(['ApexClass', 'ApexTrigger']);
// @IsTest annotation or the legacy testMethod keyword marks a test class.
const TEST_MARKER = /@IsTest|\btestmethod\b/i;
/** Pull ApexClass/ApexTrigger member names out of a package.xml manifest. */
function parseApexMembers(manifestXml) {
const names = [];
let hasWildcard = false;
for (const block of manifestXml.match(/<types>[\s\S]*?<\/types>/g) ?? []) {
const typeName = block.match(/<name>\s*([^<\s]+)\s*<\/name>/)?.[1] ?? '';
if (!APEX_TYPES.has(typeName)) {
continue;
}
for (const member of block.matchAll(/<members>\s*([^<]+?)\s*<\/members>/g)) {
const value = member[1];
if (value === undefined) {
continue;
}
if (value === '*') {
hasWildcard = true;
}
else {
names.push(value);
}
}
}
return { names, hasWildcard };
}
function findRelevantTests(request, deps) {
const manifest = deps.files.readFile(request.packageXmlPath);
const { names: changedNames, hasWildcard } = parseApexMembers(manifest);
if (changedNames.length === 0 && !hasWildcard) {
deps.logger.info('Delta contains no Apex classes or triggers — no tests to select.');
return { tests: [], testCount: 0, hasApex: false, changedApexNames: [] };
}
if (hasWildcard) {
deps.logger.warning('Delta manifest uses a wildcard Apex member — cannot scope tests; caller should run local tests.');
return { tests: [], testCount: 0, hasApex: true, changedApexNames: changedNames };
}
const sources = deps.files.listApexClassFiles(request.sourceDir).map((path) => {
const body = deps.files.readFile(path);
const fileName = path.replace(/\\/g, '/').split('/').pop() ?? '';
return { name: fileName.replace(/\.cls$/i, ''), body, isTest: TEST_MARKER.test(body) };
});
const testClasses = sources.filter((source) => source.isTest);
const changedLower = new Set(changedNames.map((name) => name.toLowerCase()));
const selected = new Set();
// A changed class that is itself a test class runs directly.
for (const test of testClasses) {
if (changedLower.has(test.name.toLowerCase())) {
selected.add(test.name);
}
}
const changedNonTest = changedNames.filter((name) => !testClasses.some((test) => test.name.toLowerCase() === name.toLowerCase()));
for (const name of changedNonTest) {
const reference = new RegExp(`\\b${escapeRegExp(name)}\\b`, 'i');
for (const test of testClasses) {
const nameMatch = request.testSuffixes.some((suffix) => test.name.toLowerCase() === `${name}${suffix}`.toLowerCase());
if (nameMatch || reference.test(test.body)) {
selected.add(test.name);
}
}
}
const tests = [...selected].sort((a, b) => a.localeCompare(b));
deps.logger.info(`Selected ${tests.length} test class(es) for ${changedNames.length} changed Apex member(s).`);
return { tests, testCount: tests.length, hasApex: true, changedApexNames: changedNames };
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/** Action seam: map validated inputs + context onto the use case. */
async function runFindTestsAction(input, context) {
try {
const result = findRelevantTests({
packageXmlPath: input.packageXml,
sourceDir: input.sourceDir,
testSuffixes: input.testSuffixes,
}, { files: (0, nodeSourceFileReader_1.createNodeSourceFileReader)(), logger: context.logger });
return (0, result_1.ok)(result);
}
catch (error) {
return (0, result_1.err)((0, errors_1.asAppError)(error));
}
}


/***/ }),

/***/ 2605:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {

"use strict";

Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.createNodeSourceFileReader = createNodeSourceFileReader;
/**
* Node `fs`-backed SourceFileReader. Kept apart from the use case so tests can
* inject an in-memory fake; `node:fs` is portable Node, not a runner API.
*/
const node_fs_1 = __nccwpck_require__(3024);
const node_path_1 = __nccwpck_require__(6760);
function createNodeSourceFileReader() {
return {
readFile(path) {
return (0, node_fs_1.readFileSync)(path, 'utf8');
},
listApexClassFiles(dir) {
const found = [];
const walk = (current) => {
for (const entry of (0, node_fs_1.readdirSync)(current, { withFileTypes: true })) {
const path = (0, node_path_1.join)(current, entry.name);
if (entry.isDirectory()) {
walk(path);
}
else if (entry.isFile() && entry.name.toLowerCase().endsWith('.cls')) {
found.push(path);
}
}
};
walk(dir);
return found;
},
};
}


/***/ }),

/***/ 4598:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {

"use strict";

Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.validateFindTestsInputs = validateFindTestsInputs;
/**
* Validate and normalize the raw sf-find-tests inputs. Portable: plain strings
* in, never touches `@actions/core`.
*/
const validation_1 = __nccwpck_require__(8968);
const DEFAULT_SUFFIXES = ['Test', '_Test', 'Tests'];
function validateFindTestsInputs(raw) {
const suffixes = (0, validation_1.parseList)(raw.testSuffixes);
return {
packageXml: (0, validation_1.requireNonEmpty)('package-xml', raw.packageXml),
sourceDir: (0, validation_1.requireNonEmpty)('source-dir', raw.sourceDir),
githubToken: (0, validation_1.requireNonEmpty)('github-token', raw.githubToken),
testSuffixes: suffixes.length > 0 ? suffixes : DEFAULT_SUFFIXES,
};
}


/***/ }),

/***/ 4342:
Expand Down Expand Up @@ -34450,7 +34620,7 @@ exports.OctokitPullRequestService = OctokitPullRequestService;
* import only from `@gforce/core`.
*/
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.validateCreateReleasePrInputs = exports.runCreateReleasePrAction = exports.renderReleaseBody = exports.createReleasePr = exports.SYNC_STRATEGIES = exports.validateSyncBranchesInputs = exports.runSyncBranchesAction = exports.syncBranches = exports.parseRepoRef = exports.OctokitGitHubService = exports.OctokitPullRequestService = exports.OctokitBranchService = exports.GitHubClient = exports.parseEnum = exports.parseList = exports.parseBoolean = exports.requireNonEmpty = exports.NoopLogger = exports.asAppError = exports.GitHubApiError = exports.ValidationError = exports.AppError = exports.isErr = exports.isOk = exports.err = exports.ok = void 0;
exports.createNodeSourceFileReader = exports.validateFindTestsInputs = exports.runFindTestsAction = exports.parseApexMembers = exports.findRelevantTests = exports.validateCreateReleasePrInputs = exports.runCreateReleasePrAction = exports.renderReleaseBody = exports.createReleasePr = exports.SYNC_STRATEGIES = exports.validateSyncBranchesInputs = exports.runSyncBranchesAction = exports.syncBranches = exports.parseRepoRef = exports.OctokitGitHubService = exports.OctokitPullRequestService = exports.OctokitBranchService = exports.GitHubClient = exports.parseEnum = exports.parseList = exports.parseBoolean = exports.requireNonEmpty = exports.NoopLogger = exports.asAppError = exports.GitHubApiError = exports.ValidationError = exports.AppError = exports.isErr = exports.isOk = exports.err = exports.ok = void 0;
// Result + errors
var result_1 = __nccwpck_require__(3860);
Object.defineProperty(exports, "ok", ({ enumerable: true, get: function () { return result_1.ok; } }));
Expand Down Expand Up @@ -34499,6 +34669,15 @@ Object.defineProperty(exports, "renderReleaseBody", ({ enumerable: true, get: fu
Object.defineProperty(exports, "runCreateReleasePrAction", ({ enumerable: true, get: function () { return createReleasePr_1.runCreateReleasePrAction; } }));
var validateCreateReleasePrInputs_1 = __nccwpck_require__(4025);
Object.defineProperty(exports, "validateCreateReleasePrInputs", ({ enumerable: true, get: function () { return validateCreateReleasePrInputs_1.validateCreateReleasePrInputs; } }));
// find-relevant-tests use case + action seam
var findRelevantTests_1 = __nccwpck_require__(7814);
Object.defineProperty(exports, "findRelevantTests", ({ enumerable: true, get: function () { return findRelevantTests_1.findRelevantTests; } }));
Object.defineProperty(exports, "parseApexMembers", ({ enumerable: true, get: function () { return findRelevantTests_1.parseApexMembers; } }));
Object.defineProperty(exports, "runFindTestsAction", ({ enumerable: true, get: function () { return findRelevantTests_1.runFindTestsAction; } }));
var validateFindTestsInputs_1 = __nccwpck_require__(4598);
Object.defineProperty(exports, "validateFindTestsInputs", ({ enumerable: true, get: function () { return validateFindTestsInputs_1.validateFindTestsInputs; } }));
var nodeSourceFileReader_1 = __nccwpck_require__(2605);
Object.defineProperty(exports, "createNodeSourceFileReader", ({ enumerable: true, get: function () { return nodeSourceFileReader_1.createNodeSourceFileReader; } }));


/***/ }),
Expand Down Expand Up @@ -34972,6 +35151,22 @@ module.exports = require("node:events");

/***/ }),

/***/ 3024:
/***/ ((module) => {

"use strict";
module.exports = require("node:fs");

/***/ }),

/***/ 6760:
/***/ ((module) => {

"use strict";
module.exports = require("node:path");

/***/ }),

/***/ 7075:
/***/ ((module) => {

Expand Down
133 changes: 133 additions & 0 deletions .github/actions/sf-delta-package/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
name: "Salesforce Delta Package"
description: "Generate a delta package.xml between two git refs using sfdx-git-delta"
author: "GForce Innovation"

inputs:
from-ref:
description: "Base git ref/SHA for the delta (e.g. the PR base or previous deploy commit)"
required: true
to-ref:
description: "Head git ref/SHA for the delta"
required: false
default: "HEAD"
output-dir:
description: "Directory the delta package is written to"
required: false
default: "delta"
source-dir:
description: "Source directory sfdx-git-delta scans"
required: false
default: "force-app"
generate-delta:
description: "Also copy the changed source files into the output dir (audit-friendly)"
required: false
default: "false"

outputs:
package-path:
description: "Path to the generated delta package.xml"
value: ${{ steps.delta.outputs.package-path }}
has-changes:
description: "true when the delta contains at least one deployable component"
value: ${{ steps.delta.outputs.has-changes }}
component-count:
description: "Total number of components in the delta package"
value: ${{ steps.delta.outputs.component-count }}

runs:
using: "composite"
steps:
# No credentials are handled by this action, so no if: always() cleanup step.
- name: Verify git refs
shell: bash
env:
FROM_REF: ${{ inputs.from-ref }}
TO_REF: ${{ inputs.to-ref }}
run: |
set -euo pipefail

# Container jobs mount the workspace with mismatched ownership
git config --global --add safe.directory "$GITHUB_WORKSPACE"

for REF in "$FROM_REF" "$TO_REF"; do
if ! git rev-parse --verify --quiet "${REF}^{commit}" > /dev/null; then
echo "::error::Git ref '$REF' cannot be resolved. Check out with 'fetch-depth: 0' so both delta refs exist locally." >&2
exit 1
fi
done

- name: Ensure sfdx-git-delta plugin
shell: bash
run: |
set -euo pipefail
if ! sf plugins inspect sfdx-git-delta > /dev/null 2>&1; then
# Preinstalled in gforceinnovation/sf-ci — this fallback costs ~30s elsewhere
echo "sfdx-git-delta not found — installing"
echo y | sf plugins install sfdx-git-delta
fi

- name: Generate delta package
id: delta
shell: bash
env:
FROM_REF: ${{ inputs.from-ref }}
TO_REF: ${{ inputs.to-ref }}
OUTPUT_DIR: ${{ inputs.output-dir }}
SOURCE_DIR: ${{ inputs.source-dir }}
GENERATE_DELTA: ${{ inputs.generate-delta }}
run: |
set -euo pipefail

mkdir -p "$OUTPUT_DIR"

EXTRA_ARGS=()
if [ "$GENERATE_DELTA" = "true" ]; then
EXTRA_ARGS+=(--generate-delta)
fi
sf sgd source delta \
--from "$FROM_REF" \
--to "$TO_REF" \
--output-dir "$OUTPUT_DIR" \
--source-dir "$SOURCE_DIR" \
"${EXTRA_ARGS[@]}"

PACKAGE_XML="$OUTPUT_DIR/package/package.xml"
if [ ! -f "$PACKAGE_XML" ]; then
echo "::error::sfdx-git-delta did not produce $PACKAGE_XML" >&2
exit 1
fi

COMPONENT_COUNT=$(grep -c "<members>" "$PACKAGE_XML" || true)
TYPES_COUNT=$(grep -c "<types>" "$PACKAGE_XML" || true)
if [ "$TYPES_COUNT" -gt 0 ]; then
HAS_CHANGES=true
else
HAS_CHANGES=false
fi

{
echo "package-path=$PACKAGE_XML"
echo "has-changes=$HAS_CHANGES"
echo "component-count=$COMPONENT_COUNT"
} >> "$GITHUB_OUTPUT"

{
echo "### Delta package (\`$FROM_REF\` → \`$TO_REF\`)"
echo ""
if [ "$HAS_CHANGES" = "true" ]; then
echo "$COMPONENT_COUNT component(s):"
echo ""
echo "| Type | Member |"
echo "|------|--------|"
awk '
/<types>/ { n = 0; delete m }
/<members>/ { line = $0; gsub(/.*<members>|<\/members>.*/, "", line); m[n++] = line }
/<name>/ { line = $0; gsub(/.*<name>|<\/name>.*/, "", line); t = line }
/<\/types>/ { for (i = 0; i < n; i++) printf "| %s | %s |\n", t, m[i] }
' "$PACKAGE_XML"
else
echo "_No deployable metadata changes._"
fi
} > "$OUTPUT_DIR/components.md"

cat "$OUTPUT_DIR/components.md" >> "$GITHUB_STEP_SUMMARY"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public class FxRateService {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@IsTest private class InvoiceServiceTest { static void covers() { FxRateService.class.getName(); } }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public class InvoicesSelector {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@IsTest private class InvoicesSelectorTest {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@IsTest private class UnrelatedTest {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>InvoicesSelector</members>
<members>FxRateService</members>
<name>ApexClass</name>
</types>
<version>65.0</version>
</Package>
Loading
Loading