Skip to content
Open
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
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI

on:
pull_request:
push:
branches:
- master

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

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

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

- name: Authenticate Dev Hub
env:
DEVHUB_AUTH_URL: ${{ secrets.DEVHUB_AUTH_URL }}
run: |
if [ -z "$DEVHUB_AUTH_URL" ]; then
echo "DEVHUB_AUTH_URL secret is not set"
exit 1
fi
printenv DEVHUB_AUTH_URL > devhub-auth-url.txt
sf org login sfdx-url --sfdx-url-file devhub-auth-url.txt --alias devhub --set-default-dev-hub
rm -f devhub-auth-url.txt

- name: Create scratch org
run: sf org create scratch --definition-file config/project-scratch-def.json --alias ci --duration-days 1 --wait 10 --target-dev-hub devhub

- name: Deploy force-app
run: sf project deploy start --source-dir force-app --target-org ci --wait 10

- name: Run Apex tests
run: sf apex run test --target-org ci --wait 10 --code-coverage --result-format human

- name: Delete scratch org
if: always()
run: sf org delete scratch --target-org ci --no-prompt
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

# Salesforce cache
.sfdx/
.sf/

# Cursor
.cursor/

# Logs
logs
Expand Down
1 change: 1 addition & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"plugins": ["prettier-plugin-apex"],
"trailingComma": "none",
"overrides": [
{
Expand Down
8 changes: 5 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{
"salesforcedx-vscode-core.push-or-deploy-on-save.enabled": true,
"salesforcedx-vscode-core.push-or-deploy-on-save.enabled": false,
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/.sfdx": true
"**/.sfdx": true,
"**/.sf": true
},
"editor.tabSize": 4,
"editor.formatOnSave": true,
"editor.formatOnSaveTimeout": 10000,
"eslint.enable": false
"eslint.enable": false,
"xml.preferences.showSchemaDocumentationType": "none"
}
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Changelog

All notable changes to Scheduled Test Runner will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- GitHub Actions CI workflow runs Apex tests in a scratch org on pull requests and pushes to `master` ([#19](https://github.com/callawaycloud/ScheduledTestRunner/issues/19))
- Test run lifecycle status (`Status__c`) and error message (`Error_Message__c`) fields ([#18](https://github.com/callawaycloud/ScheduledTestRunner/issues/18))
- Custom tabs and list views (Unprocessed Runs, Failed Methods) ([#18](https://github.com/callawaycloud/ScheduledTestRunner/issues/18))
- Record-triggered Flow (`Test Run Failure Notification`) for failure emails; ships inactive (`Draft`) under `notifications/workflow-email` ([#20](https://github.com/callawaycloud/ScheduledTestRunner/issues/20))

### Changed
- README documents permission set assignment, automatic cleanup, scratch-org development, `sf` CLI package commands, and the `DEVHUB_AUTH_URL` CI secret ([#19](https://github.com/callawaycloud/ScheduledTestRunner/issues/19))
- Apex, email, and Visualforce metadata API versions aligned to 59.0; `versionName` matches `0.1.7` ([#19](https://github.com/callawaycloud/ScheduledTestRunner/issues/19))
- `package.json` license is MIT; `npm test` runs Apex tests via Salesforce CLI ([#19](https://github.com/callawaycloud/ScheduledTestRunner/issues/19))
- Prettier Apex plugin wired in `.prettierrc`; deploy-on-save disabled in VS Code settings ([#19](https://github.com/callawaycloud/ScheduledTestRunner/issues/19))
- Scratch org definition uses a generic org name and current settings shape; `.sf/` is gitignored ([#19](https://github.com/callawaycloud/ScheduledTestRunner/issues/19))
- Retention prune uses a Datetime bind and runs in a Queueable; stuck unprocessed runs are abandoned ([#17](https://github.com/callawaycloud/ScheduledTestRunner/issues/17))
- First-failure tracking keys on class + method; Skip is not counted as a failure ([#17](https://github.com/callawaycloud/ScheduledTestRunner/issues/17))
- Unit test discovery excludes managed package classes via SOSL `NamespacePrefix = null` ([#18](https://github.com/callawaycloud/ScheduledTestRunner/issues/18))
- `Parent_Job_Ids__c` is a long text area (up to 32,768 characters) instead of a unique external id ([#18](https://github.com/callawaycloud/ScheduledTestRunner/issues/18))
- Permission set FLS, layouts, and the `New Failure` field label match the packaged fields ([#18](https://github.com/callawaycloud/ScheduledTestRunner/issues/18))
- Workflow Rule replaced by a Flow-driven email alert; placeholder `ccEmails` removed ([#20](https://github.com/callawaycloud/ScheduledTestRunner/issues/20))
- Failure email is UTF-8, uses `(NEW)` instead of an emoji, org-domain URLs, and caps displayed rows at 200 ([#20](https://github.com/callawaycloud/ScheduledTestRunner/issues/20))

## [1.0.0] - 2020-09-10

### Added
- Initial project scaffolding

[Unreleased]: https://github.com/callawaycloud/ScheduledTestRunner/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/callawaycloud/ScheduledTestRunner/releases/tag/v1.0.0
94 changes: 66 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
# Scheduled Test Runner

A Salesforce package to help monitor organization's unit tests health.
A Salesforce package to help monitor your organization's unit test health.

<img width="1264" alt="Test_Run__2020-05-05_03_00_07___Salesforce" src="https://user-images.githubusercontent.com/5217568/81127689-3c223900-8efc-11ea-94ab-7bfb34a4c957.png">

- Setup to run via scheduled process
- Test results are store in objects
- `Test_Run__c`: A test Job instance
- `Test_Run_Method_Result__c`: An individual test method result
- Results are pruned after 30 days
- Tracks "First Failure" to help with debugging / noise reduction
- Automaticly builds Audit Log query to help troubleshoot what changes might have caused a test to fail
- Captures Run Time, which can help identify performance issues or slow tests
- Setup to run via scheduled process
- Test results are stored in objects
- `Test_Run__c`: A test job instance
- `Test_Run_Method_Result__c`: An individual test method result
- Tracks "First Failure" to help with debugging / noise reduction
- Automatically builds Audit Log query to help troubleshoot what changes might have caused a test to fail
- Captures Run Time, which can help identify performance issues or slow tests

## Install

[Unlocked Package Install Link](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t1C000000lIXrQAM)

## Setup

### Permission set

Assign the **Test Runner** permission set (`Test_Runner`) to users who need the UI (tabs, list views, field-level security). Missing FLS does not break scheduled Apex—the schedulers run as the user who scheduled them.

### Scheduling

Schedule **TestRunScheduler** and **TestRunProcessor** as a user who can query Apex tests (typically someone with **Author Apex** or **System Administrator**).

1. Schedule the unit test run frequency (example below runs daily at 3am):

```java
Expand All @@ -39,42 +44,75 @@ System.Schedule('Test Processor', sch, testProcessor);

**NOTES:**

- If you find your test runs are failing inconsistently, you may need to [disabling "parallel" test runs](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_best_practices.htm)
- If you find your test runs are failing inconsistently, you may need to [disable "parallel" test runs](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_best_practices.htm).
- Cleanup is automatic: **TestRunScheduler** enqueues **TestRunCleanup**, which prunes processed runs after 30 days and abandons unprocessed runs after 2 days.

### Sending Notifications

![Screen Shot 2020-07-31 at 1 41 14 PM](https://user-images.githubusercontent.com/5217568/89072011-08a54e00-d335-11ea-9ba9-10c5a03cb8ee.png)

To keep things as flexible & upgradable possible, we've decided not to package any notification logic with the "unlocked" package.
To keep things as flexible and upgradable as possible, we've decided not to package any notification logic with the unlocked package.

Since the results are stored in an object, you can you use tool of choice to receive notifications about failures.
Since the results are stored in an object, you can use a tool of choice to receive notifications about failures.

However, we have included a simple workflow to send an email when a test fails.
We include optional notification metadata (email template, Visualforce component, email alert, and a record-triggered Flow) under `notifications/workflow-email`. The Flow ships **inactive** (`Draft`) so you can configure recipients before it sends mail.

**To install workflow email alert:**
**To deploy notification metadata:**

- run `sfdx force:source:convert -r notifications/workflow-email -d ./dist/workflow-email` to create a metadata package
- run `sfdx force:mdapi:deploy -d ./dist/workflow-email/ -w -1` to deploy
- Update Workflow Action "Test Run Failure Notification Alert", to include your email addresses
```bash
sf project deploy start --source-dir notifications/workflow-email --target-org <alias>
```

We may add more prebuilt notification methods in the future.
**After deploy:**

1. Open **Setup → Email Alerts** and review **Test Run Failure Notification Alert**. The alert defaults to the Test Run **owner**; admins can add more recipients.
2. Prefer an **org-wide email address** as the alert sender (Setup → Organization-Wide Addresses). Metadata cannot ship an org-specific org-wide address Id, so `senderType` stays `CurrentUser` until you switch it in Setup.
3. Open **Setup → Flows**, open **Test Run Failure Notification**, review entry criteria (`Processed__c = true` and `Test_Failures__c > 0`, with a PRIORVALUE guard on `Processed__c`), then **Activate** the Flow when the alert is configured.

**TIP: If you only want to be notified of new failures, update the workflow to send if `Test_Run__r.New_Failures__c > 0`**
**TIP: If you only want to be notified of new failures, update the Flow entry criteria to use `Test_Run__c.New_Failures__c > 0` (field on `Test_Run__c`, not `Test_Run__r`).**

We may add more prebuilt notification methods in the future.

## Development

### Scratch org

```bash
sf org create scratch --definition-file config/project-scratch-def.json --alias str-scratch --duration-days 7 --wait 10
sf project deploy start --source-dir force-app --target-org str-scratch --wait 10
sf org assign permset --name Test_Runner --target-org str-scratch
sf apex run test --target-org str-scratch --wait 10 --code-coverage --result-format human
```

Or run tests via npm:

```bash
npm test
```

### Continuous integration

GitHub Actions runs Apex tests on pull requests and pushes to `master`. The workflow requires a repository secret:

- **`DEVHUB_AUTH_URL`** — SFDX auth URL for the Dev Hub org (used to create ephemeral scratch orgs).

If the secret is not configured, the CI job will fail until it is set.

### Releasing a new version

Make your updates, release a new version:
Make your updates, then create and promote a package version:

- open `sfdx-project.json`
- increment `versionName` and `versionNumber` respectively
- save
- `sfdx force:package:version:create -p TestRunner -d force-app -x --wait 10 -v CCC-SFDC-Production`
1. Open `sfdx-project.json` and increment `versionName` and `versionNumber` as needed.
2. Create a package version:

### Promote
```bash
sf package version create --package TestRunner --path force-app --installation-key-bypass --wait 10 --target-dev-hub <devhub-alias>
```

Once you are ready to release the updated package, use the 04t\* Id from the new release to promote:
3. Promote when ready:

```bash
sf package version promote --package 04t... --target-dev-hub <devhub-alias>
```

- `sfdx force:package:version:promote -p 04t\*`
- update `README.mdd` install step with the new install URL
4. Update the install URL in this README with the new `04t` Id from the promoted version.
8 changes: 4 additions & 4 deletions config/project-scratch-def.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"orgName": "charlesjonas Company",
"orgName": "Scheduled Test Runner",
"edition": "Developer",
"features": [],
"hasSampleData": false,
"settings": {
"orgPreferenceSettings": {
"s1DesktopEnabled": true
"lightningExperienceSettings": {
"enableS1DesktopEnabled": true
}
}
}
73 changes: 73 additions & 0 deletions force-app/main/default/classes/TestRunCleanup.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Async cleanup for Test_Run__c records.
*
* Prune and stuck-run abandonment run in a separate Queueable so they do not
* share a transaction with ApexTestQueueItem enqueue/DML. Cascade deletes of
* Test_Run_Method_Result__c children count toward DML row limits and can roll
* back the new test run when cleanup runs inline with enqueue.
*/
public with sharing class TestRunCleanup implements Queueable {
private static final Integer PRUNE_BATCH_SIZE = 200;
private static final Integer PRUNE_DAYS = 30;
private static final Integer STUCK_DAYS = 2;

/**
* Abandons stuck runs, then prunes processed runs older than 30 days.
*/
public void execute(QueueableContext context) {
abandonStuckRuns();
pruneOldRuns();
}

/**
* Marks unprocessed runs older than two days as processed so they stop
* blocking TestRunProcessor and become eligible for 30-day prune.
*/
private void abandonStuckRuns() {
Datetime stuckCutoff = Datetime.now().addDays(-STUCK_DAYS);
List<Test_Run__c> stuckRuns = [
SELECT Id
FROM Test_Run__c
WHERE Processed__c = false
AND CreatedDate < :stuckCutoff
];

if (stuckRuns.isEmpty()) {
return;
}

for (Test_Run__c stuckRun : stuckRuns) {
stuckRun.Processed__c = true;
stuckRun.Status__c = 'Abandoned';
}

update stuckRuns;
}

/**
* Deletes processed Test_Run__c records older than 30 days in batches of
* at most 200 parents per DML to stay within cascade-delete row limits.
*/
private void pruneOldRuns() {
Datetime cutoff = Datetime.now().addDays(-PRUNE_DAYS);
List<Test_Run__c> toDelete = [
SELECT Id
FROM Test_Run__c
WHERE Processed__c = true
AND CreatedDate < :cutoff
LIMIT :PRUNE_BATCH_SIZE
];

while (!toDelete.isEmpty()) {
delete toDelete;

toDelete = [
SELECT Id
FROM Test_Run__c
WHERE Processed__c = true
AND CreatedDate < :cutoff
LIMIT :PRUNE_BATCH_SIZE
];
}
}
}
5 changes: 5 additions & 0 deletions force-app/main/default/classes/TestRunCleanup.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>59.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading
Loading