diff --git a/.github/actions/awaitStagingDeploys/action.yml b/.github/actions/awaitStagingDeploys/action.yml
index ec8b714dd7f5..b5cf151f3f19 100644
--- a/.github/actions/awaitStagingDeploys/action.yml
+++ b/.github/actions/awaitStagingDeploys/action.yml
@@ -4,6 +4,9 @@ inputs:
GITHUB_TOKEN:
description: Auth token for New Expensify Github
required: true
+ TAG:
+ description: If provided, this action will only wait for a deploy matching this tag.
+ required: false
runs:
using: 'node12'
main: './index.js'
diff --git a/.github/actions/awaitStagingDeploys/awaitStagingDeploys.js b/.github/actions/awaitStagingDeploys/awaitStagingDeploys.js
index 540bfd1048bb..d71bf68c0812 100644
--- a/.github/actions/awaitStagingDeploys/awaitStagingDeploys.js
+++ b/.github/actions/awaitStagingDeploys/awaitStagingDeploys.js
@@ -1,8 +1,10 @@
const _ = require('underscore');
+const ActionUtils = require('../../libs/ActionUtils');
const GitHubUtils = require('../../libs/GithubUtils');
const {promiseDoWhile} = require('../../libs/promiseWhile');
function run() {
+ const tag = ActionUtils.getStringInput('TAG', {required: false});
let currentStagingDeploys = [];
return promiseDoWhile(
() => !_.isEmpty(currentStagingDeploys),
@@ -14,20 +16,24 @@ function run() {
repo: GitHubUtils.APP_REPO,
workflow_id: 'platformDeploy.yml',
event: 'push',
+ branch: tag,
}),
- // These have the potential to become active deploys, so we need to wait for them to finish as well
+ // These have the potential to become active deploys, so we need to wait for them to finish as well (unless we're looking for a specific tag)
// In this context, we'll refer to unresolved preDeploy workflow runs as staging deploys as well
- GitHubUtils.octokit.actions.listWorkflowRuns({
+ !tag && GitHubUtils.octokit.actions.listWorkflowRuns({
owner: GitHubUtils.GITHUB_OWNER,
repo: GitHubUtils.APP_REPO,
workflow_id: 'preDeploy.yml',
}),
])
- .then(responses => [
- ...responses[0].data.workflow_runs,
- ...responses[1].data.workflow_runs,
- ])
+ .then((responses) => {
+ const workflowRuns = responses[0].data.workflow_runs;
+ if (!tag) {
+ workflowRuns.push(...responses[1].data.workflow_runs);
+ }
+ return workflowRuns;
+ })
.then(workflowRuns => currentStagingDeploys = _.filter(workflowRuns, workflowRun => workflowRun.status !== 'completed'))
.then(() => console.log(
_.isEmpty(currentStagingDeploys)
diff --git a/.github/actions/awaitStagingDeploys/index.js b/.github/actions/awaitStagingDeploys/index.js
index 6b504c7e868f..a3b202b6bd12 100644
--- a/.github/actions/awaitStagingDeploys/index.js
+++ b/.github/actions/awaitStagingDeploys/index.js
@@ -9,10 +9,12 @@ module.exports =
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const _ = __nccwpck_require__(3571);
+const ActionUtils = __nccwpck_require__(970);
const GitHubUtils = __nccwpck_require__(7999);
const {promiseDoWhile} = __nccwpck_require__(4502);
function run() {
+ const tag = ActionUtils.getStringInput('TAG', {required: false});
let currentStagingDeploys = [];
return promiseDoWhile(
() => !_.isEmpty(currentStagingDeploys),
@@ -24,20 +26,24 @@ function run() {
repo: GitHubUtils.APP_REPO,
workflow_id: 'platformDeploy.yml',
event: 'push',
+ branch: tag,
}),
- // These have the potential to become active deploys, so we need to wait for them to finish as well
+ // These have the potential to become active deploys, so we need to wait for them to finish as well (unless we're looking for a specific tag)
// In this context, we'll refer to unresolved preDeploy workflow runs as staging deploys as well
- GitHubUtils.octokit.actions.listWorkflowRuns({
+ !tag && GitHubUtils.octokit.actions.listWorkflowRuns({
owner: GitHubUtils.GITHUB_OWNER,
repo: GitHubUtils.APP_REPO,
workflow_id: 'preDeploy.yml',
}),
])
- .then(responses => [
- ...responses[0].data.workflow_runs,
- ...responses[1].data.workflow_runs,
- ])
+ .then((responses) => {
+ const workflowRuns = responses[0].data.workflow_runs;
+ if (!tag) {
+ workflowRuns.push(...responses[1].data.workflow_runs);
+ }
+ return workflowRuns;
+ })
.then(workflowRuns => currentStagingDeploys = _.filter(workflowRuns, workflowRun => workflowRun.status !== 'completed'))
.then(() => console.log(
_.isEmpty(currentStagingDeploys)
@@ -58,6 +64,52 @@ if (require.main === require.cache[eval('__filename')]) {
module.exports = run;
+/***/ }),
+
+/***/ 970:
+/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
+
+const core = __nccwpck_require__(2186);
+
+/**
+ * Safely parse a JSON input to a GitHub Action.
+ *
+ * @param {String} name - The name of the input.
+ * @param {Object} options - Options to pass to core.getInput
+ * @param {*} [defaultValue] - A default value to provide for the input.
+ * Not required if the {required: true} option is given in the second arg to this function.
+ * @returns {any}
+ */
+function getJSONInput(name, options, defaultValue = undefined) {
+ const input = core.getInput(name, options);
+ if (input) {
+ return JSON.parse(input);
+ }
+ return defaultValue;
+}
+
+/**
+ * Safely access a string input to a GitHub Action, or fall back on a default if the string is empty.
+ *
+ * @param {String} name
+ * @param {Object} options
+ * @param {*} [defaultValue]
+ * @returns {string|undefined}
+ */
+function getStringInput(name, options, defaultValue = undefined) {
+ const input = core.getInput(name, options);
+ if (!input) {
+ return defaultValue;
+ }
+ return input;
+}
+
+module.exports = {
+ getJSONInput,
+ getStringInput,
+};
+
+
/***/ }),
/***/ 7999:
diff --git a/.github/actions/getDeployPullRequestList/index.js b/.github/actions/getDeployPullRequestList/index.js
index 0ad91cbdd8d6..3b74abe4ab01 100644
--- a/.github/actions/getDeployPullRequestList/index.js
+++ b/.github/actions/getDeployPullRequestList/index.js
@@ -96,8 +96,25 @@ function getJSONInput(name, options, defaultValue = undefined) {
return defaultValue;
}
+/**
+ * Safely access a string input to a GitHub Action, or fall back on a default if the string is empty.
+ *
+ * @param {String} name
+ * @param {Object} options
+ * @param {*} [defaultValue]
+ * @returns {string|undefined}
+ */
+function getStringInput(name, options, defaultValue = undefined) {
+ const input = core.getInput(name, options);
+ if (!input) {
+ return defaultValue;
+ }
+ return input;
+}
+
module.exports = {
getJSONInput,
+ getStringInput,
};
diff --git a/.github/actions/getPullRequestDetails/index.js b/.github/actions/getPullRequestDetails/index.js
index b3f885b04301..229a9d2655e0 100644
--- a/.github/actions/getPullRequestDetails/index.js
+++ b/.github/actions/getPullRequestDetails/index.js
@@ -129,8 +129,25 @@ function getJSONInput(name, options, defaultValue = undefined) {
return defaultValue;
}
+/**
+ * Safely access a string input to a GitHub Action, or fall back on a default if the string is empty.
+ *
+ * @param {String} name
+ * @param {Object} options
+ * @param {*} [defaultValue]
+ * @returns {string|undefined}
+ */
+function getStringInput(name, options, defaultValue = undefined) {
+ const input = core.getInput(name, options);
+ if (!input) {
+ return defaultValue;
+ }
+ return input;
+}
+
module.exports = {
getJSONInput,
+ getStringInput,
};
diff --git a/.github/actions/getReleaseBody/index.js b/.github/actions/getReleaseBody/index.js
index ae97afa70929..2dfb848d3579 100644
--- a/.github/actions/getReleaseBody/index.js
+++ b/.github/actions/getReleaseBody/index.js
@@ -47,8 +47,25 @@ function getJSONInput(name, options, defaultValue = undefined) {
return defaultValue;
}
+/**
+ * Safely access a string input to a GitHub Action, or fall back on a default if the string is empty.
+ *
+ * @param {String} name
+ * @param {Object} options
+ * @param {*} [defaultValue]
+ * @returns {string|undefined}
+ */
+function getStringInput(name, options, defaultValue = undefined) {
+ const input = core.getInput(name, options);
+ if (!input) {
+ return defaultValue;
+ }
+ return input;
+}
+
module.exports = {
getJSONInput,
+ getStringInput,
};
diff --git a/.github/actions/markPullRequestsAsDeployed/index.js b/.github/actions/markPullRequestsAsDeployed/index.js
index 66ef283d3506..dd03f35c1dde 100644
--- a/.github/actions/markPullRequestsAsDeployed/index.js
+++ b/.github/actions/markPullRequestsAsDeployed/index.js
@@ -180,8 +180,25 @@ function getJSONInput(name, options, defaultValue = undefined) {
return defaultValue;
}
+/**
+ * Safely access a string input to a GitHub Action, or fall back on a default if the string is empty.
+ *
+ * @param {String} name
+ * @param {Object} options
+ * @param {*} [defaultValue]
+ * @returns {string|undefined}
+ */
+function getStringInput(name, options, defaultValue = undefined) {
+ const input = core.getInput(name, options);
+ if (!input) {
+ return defaultValue;
+ }
+ return input;
+}
+
module.exports = {
getJSONInput,
+ getStringInput,
};
diff --git a/.github/actions/triggerWorkflowAndWait/index.js b/.github/actions/triggerWorkflowAndWait/index.js
index 079b4560c934..028435e30996 100644
--- a/.github/actions/triggerWorkflowAndWait/index.js
+++ b/.github/actions/triggerWorkflowAndWait/index.js
@@ -191,8 +191,25 @@ function getJSONInput(name, options, defaultValue = undefined) {
return defaultValue;
}
+/**
+ * Safely access a string input to a GitHub Action, or fall back on a default if the string is empty.
+ *
+ * @param {String} name
+ * @param {Object} options
+ * @param {*} [defaultValue]
+ * @returns {string|undefined}
+ */
+function getStringInput(name, options, defaultValue = undefined) {
+ const input = core.getInput(name, options);
+ if (!input) {
+ return defaultValue;
+ }
+ return input;
+}
+
module.exports = {
getJSONInput,
+ getStringInput,
};
diff --git a/.github/libs/ActionUtils.js b/.github/libs/ActionUtils.js
index 91d77f0dbef9..6f27150955c3 100644
--- a/.github/libs/ActionUtils.js
+++ b/.github/libs/ActionUtils.js
@@ -17,6 +17,23 @@ function getJSONInput(name, options, defaultValue = undefined) {
return defaultValue;
}
+/**
+ * Safely access a string input to a GitHub Action, or fall back on a default if the string is empty.
+ *
+ * @param {String} name
+ * @param {Object} options
+ * @param {*} [defaultValue]
+ * @returns {string|undefined}
+ */
+function getStringInput(name, options, defaultValue = undefined) {
+ const input = core.getInput(name, options);
+ if (!input) {
+ return defaultValue;
+ }
+ return input;
+}
+
module.exports = {
getJSONInput,
+ getStringInput,
};
diff --git a/.github/scripts/validateActionsAndWorkflows.sh b/.github/scripts/validateActionsAndWorkflows.sh
index e22308c735fd..35aeeca07524 100755
--- a/.github/scripts/validateActionsAndWorkflows.sh
+++ b/.github/scripts/validateActionsAndWorkflows.sh
@@ -11,10 +11,10 @@ curl https://json.schemastore.org/github-action.json --output ./tempSchemas/gith
curl https://json.schemastore.org/github-workflow.json --output ./tempSchemas/github-workflow.json --silent || exit 1
# Validate the actions and workflows using the JSON schemas and ajv https://github.com/ajv-validator/ajv-cli
-find ./actions/ -type f -name "*.yml" -print0 | xargs -0 -I file ajv -s ./tempSchemas/github-action.json -d file --strict=false || EXIT_CODE=1
-find ./workflows/ -type f -name "*.yml" -print0 | xargs -0 -I file ajv -s ./tempSchemas/github-workflow.json -d file --strict=false || EXIT_CODE=1
+find ./actions -type f -name "*.yml" -print0 | xargs -0 -I file ajv -s ./tempSchemas/github-action.json -d file --strict=false || EXIT_CODE=1
+find ./workflows -type f -name "*.yml" -print0 | xargs -0 -I file ajv -s ./tempSchemas/github-workflow.json -d file --strict=false || EXIT_CODE=1
-if (( $EXIT_CODE != 0 )); then
+if (( "$EXIT_CODE" != 0 )); then
exit $EXIT_CODE
fi
diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml
index ca0ce2d83ffb..eec346d1c8a5 100644
--- a/.github/workflows/preDeploy.yml
+++ b/.github/workflows/preDeploy.yml
@@ -133,8 +133,11 @@ jobs:
WORKFLOW: updateProtectedBranch.yml
INPUTS: '{ "TARGET_BRANCH": "staging" }'
+ - name: Determine if this pull request will be cherry-picked
+ run: echo "DO_CHERRY_PICK=${{ fromJSON(needs.chooseDeployActions.outputs.isStagingDeployLocked) && fromJSON(needs.chooseDeployActions.outputs.shouldCherryPick) }}" >> "$GITHUB_ENV"
+
- name: Cherry pick to staging
- if: ${{ fromJSON(needs.chooseDeployActions.outputs.isStagingDeployLocked) && fromJSON(needs.chooseDeployActions.outputs.shouldCherryPick) }}
+ if: ${{ env.DO_CHERRY_PICK }}
uses: Expensify/App/.github/actions/triggerWorkflowAndWait@main
with:
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
@@ -157,6 +160,33 @@ jobs:
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
NPM_VERSION: ${{ env.NEW_VERSION }}
+ - name: Comment in StagingDeployCash to alert Applause that a new pull request has been cherry-picked
+ if: ${{ env.DO_CHERRY_PICK }}
+ run: |
+ PR_URL="https://github.com/Expensify/App/pull/${{ needs.chooseDeployActions.outputs.mergedPullRequest }}"
+ printf -v COMMENT ":clap: Heads up @Expensify/applauseleads :clap:\nA [new pull request](%s) has been :cherries: cherry-picked :cherries: to staging, and will be deployed to staging in version \`%s\` :rocket:" "$PR_URL" ${{ env.NEW_VERSION }}
+ gh issue comment \
+ "$(gh issue list --label StagingDeployCash --json number --jq '.[0].number')" \
+ --body "$COMMENT"
+ env:
+ GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
+
+ - name: Wait for staging deploys to finish
+ if: ${{ env.DO_CHERRY_PICK }}
+ uses: Expensify/App/.github/actions/awaitStagingDeploys@main
+ with:
+ GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
+ TAG: ${{ env.NEW_VERSION }}
+
+ - name: Comment in StagingDeployCash to alert Applause that cherry-picked pull request has been deployed.
+ if: ${{ env.DO_CHERRY_PICK }}
+ run: |
+ gh issue comment \
+ "$(gh issue list --label StagingDeployCash --json number --jq '.[0].number')" \
+ --body ":tada: All set?…You bet! @Expensify/applauseleads https://github.com/Expensify/App/releases/tag/${{ env.NEW_VERSION }} has been deployed to staging :tada:"
+ env:
+ GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
+
# This Slack step is duplicated in all workflows, if you make a change to this step, make sure to update all
# the other workflows with the same change
- uses: 8398a7/action-slack@v3
@@ -178,7 +208,7 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
- # Check if actor is member of Expensify organization by looking for expensify-expensify team
+ # Check if actor is member of Expensify organization by looking for expensify-expensify team
isExpensifyEmployee:
runs-on: ubuntu-latest
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 477dfc9dcb61..6a09c478da9d 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -152,8 +152,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001013902
- versionName "1.1.39-2"
+ versionCode 1001013903
+ versionName "1.1.39-3"
}
splits {
abi {
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index f4b6641979ca..0cb9f7778976 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -31,7 +31,7 @@
CFBundleVersion
- 1.1.39.2
+ 1.1.39.3
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index c928da93cf49..10aea1b68105 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.1.39.2
+ 1.1.39.3
diff --git a/package-lock.json b/package-lock.json
index 1ee3214bfc16..198fbf038cca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.1.39-2",
+ "version": "1.1.39-3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index 4df3779c520d..d82a0f5fd28f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.1.39-2",
+ "version": "1.1.39-3",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/tests/unit/awaitStagingDeploysTest.js b/tests/unit/awaitStagingDeploysTest.js
index 3ed502ac51e2..8b4536ee7b05 100644
--- a/tests/unit/awaitStagingDeploysTest.js
+++ b/tests/unit/awaitStagingDeploysTest.js
@@ -1,6 +1,7 @@
/**
* @jest-environment node
*/
+const core = require('@actions/core');
const _ = require('underscore');
const run = require('../../.github/actions/awaitStagingDeploys/awaitStagingDeploys');
const GitHubUtils = require('../../.github/libs/GithubUtils');
@@ -10,6 +11,9 @@ const TEST_POLL_RATE = 1;
const COMPLETED_WORKFLOW = {status: 'completed'};
const INCOMPLETE_WORKFLOW = {status: 'in_progress'};
+const consoleSpy = jest.spyOn(console, 'log');
+const mockGetInput = jest.fn();
+const mockListPlatformDeploysForTag = jest.fn();
const mockListPlatformDeploys = jest.fn();
const mockListPreDeploys = jest.fn();
const mockListWorkflowRuns = jest.fn().mockImplementation((args) => {
@@ -19,6 +23,10 @@ const mockListWorkflowRuns = jest.fn().mockImplementation((args) => {
return defaultReturn;
}
+ if (!_.isUndefined(args.branch)) {
+ return mockListPlatformDeploysForTag();
+ }
+
if (args.workflow_id === 'platformDeploy.yml') {
return mockListPlatformDeploys();
}
@@ -31,6 +39,9 @@ const mockListWorkflowRuns = jest.fn().mockImplementation((args) => {
});
beforeAll(() => {
+ // Mock core module
+ core.getInput = mockGetInput;
+
// Mock octokit module
const mocktokit = {
actions: {
@@ -41,8 +52,15 @@ beforeAll(() => {
GitHubUtils.POLL_RATE = TEST_POLL_RATE;
});
+beforeEach(() => {
+ consoleSpy.mockClear();
+});
+
describe('awaitStagingDeploys', () => {
test('Should wait for all running staging deploys to finish', () => {
+ mockGetInput.mockImplementation(() => undefined);
+
+ // First ping
mockListPlatformDeploys.mockResolvedValueOnce({
data: {
workflow_runs: [
@@ -57,6 +75,8 @@ describe('awaitStagingDeploys', () => {
workflow_runs: [],
},
});
+
+ // Second ping
mockListPlatformDeploys.mockResolvedValueOnce({
data: {
workflow_runs: [
@@ -71,6 +91,8 @@ describe('awaitStagingDeploys', () => {
workflow_runs: [],
},
});
+
+ // Third ping
mockListPlatformDeploys.mockResolvedValueOnce({
data: {
workflow_runs: [
@@ -87,6 +109,8 @@ describe('awaitStagingDeploys', () => {
],
},
});
+
+ // Fourth ping
mockListPlatformDeploys.mockResolvedValueOnce({
data: {
workflow_runs: [
@@ -104,7 +128,6 @@ describe('awaitStagingDeploys', () => {
},
});
- const consoleSpy = jest.spyOn(console, 'log');
return run()
.then(() => {
expect(consoleSpy).toHaveBeenCalledTimes(4);
@@ -114,4 +137,93 @@ describe('awaitStagingDeploys', () => {
expect(consoleSpy).toHaveBeenLastCalledWith('No current staging deploys found');
});
});
+
+ test('Should only wait for a specific staging deploy to finish', () => {
+ mockGetInput.mockImplementation(() => 'my-tag');
+
+ // First ping
+ mockListPlatformDeploysForTag.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ INCOMPLETE_WORKFLOW,
+ ],
+ },
+ });
+ mockListPlatformDeploys.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ INCOMPLETE_WORKFLOW,
+ INCOMPLETE_WORKFLOW,
+ ],
+ },
+ });
+ mockListPreDeploys.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ INCOMPLETE_WORKFLOW,
+ INCOMPLETE_WORKFLOW,
+ ],
+ },
+ });
+
+ // Second ping
+ mockListPlatformDeploysForTag.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ INCOMPLETE_WORKFLOW,
+ ],
+ },
+ });
+ mockListPlatformDeploys.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ INCOMPLETE_WORKFLOW,
+ COMPLETED_WORKFLOW,
+ ],
+ },
+ });
+ mockListPreDeploys.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ COMPLETED_WORKFLOW,
+ COMPLETED_WORKFLOW,
+ ],
+ },
+ });
+
+ // Third ping
+ mockListPlatformDeploysForTag.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ COMPLETED_WORKFLOW,
+ ],
+ },
+ });
+ mockListPlatformDeploys.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ INCOMPLETE_WORKFLOW,
+ COMPLETED_WORKFLOW,
+ INCOMPLETE_WORKFLOW,
+ ],
+ },
+ });
+ mockListPreDeploys.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ COMPLETED_WORKFLOW,
+ COMPLETED_WORKFLOW,
+ INCOMPLETE_WORKFLOW,
+ ],
+ },
+ });
+
+ return run()
+ .then(() => {
+ expect(consoleSpy).toHaveBeenCalledTimes(3);
+ expect(consoleSpy).toHaveBeenNthCalledWith(1, 'Found 1 staging deploy still running...');
+ expect(consoleSpy).toHaveBeenNthCalledWith(2, 'Found 1 staging deploy still running...');
+ expect(consoleSpy).toHaveBeenLastCalledWith('No current staging deploys found');
+ });
+ });
});