Skip to content

Commit 78ecb3d

Browse files
authored
chore: new environment variable to builds with gh actions (#26668)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** OTA was broken with old environment variables check. This PR aims to solve that by introdocing a new environment variable. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how several services choose API endpoints/environments by switching from `GITHUB_ACTIONS`/`E2E` checks to a new `BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY` flag, which could route builds to different backend environments if misconfigured. > > **Overview** > Introduces a new build-time env flag, `BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY`, and uses it to decide when to take **build-provided** URLs/environments (from `builds.yml`) versus deriving them from `METAMASK_ENVIRONMENT`. > > Updates Baanx Card URL mapping, ramps SDK environment selection (Aggregator + Deposit), ramps controller init, and rewards API URL override logic to key off this flag, and refreshes/adjusts tests accordingly (including removing the previous special-casing tied to `E2E`). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4249918. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 67febaf commit 78ecb3d

12 files changed

Lines changed: 67 additions & 136 deletions

File tree

app/components/UI/Card/util/mapBaanxApiUrl.test.ts

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,26 @@ import { getDefaultBaanxApiBaseUrlForMetaMaskEnv } from './mapBaanxApiUrl';
33

44
describe('getDefaultBaanxApiBaseUrlForMetaMaskEnv', () => {
55
const originalBaanxUrl = process.env.BAANX_API_URL;
6-
const originalGitHubActions = process.env.GITHUB_ACTIONS;
7-
const originalE2e = process.env.E2E;
6+
const originalBuildsEnabled =
7+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY;
88

99
afterEach(() => {
1010
if (originalBaanxUrl !== undefined) {
1111
process.env.BAANX_API_URL = originalBaanxUrl;
1212
} else {
1313
delete process.env.BAANX_API_URL;
1414
}
15-
if (originalGitHubActions !== undefined) {
16-
process.env.GITHUB_ACTIONS = originalGitHubActions;
15+
if (originalBuildsEnabled !== undefined) {
16+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY =
17+
originalBuildsEnabled;
1718
} else {
18-
delete process.env.GITHUB_ACTIONS;
19-
}
20-
if (originalE2e !== undefined) {
21-
process.env.E2E = originalE2e;
22-
} else {
23-
delete process.env.E2E;
19+
delete process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY;
2420
}
2521
});
2622

27-
describe('when GITHUB_ACTIONS (builds.yml path)', () => {
23+
describe('when BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY (builds.yml path)', () => {
2824
beforeEach(() => {
29-
process.env.GITHUB_ACTIONS = 'true';
30-
delete process.env.E2E;
25+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY = 'true';
3126
});
3227

3328
it('returns BAANX_API_URL from environment when set', () => {
@@ -53,23 +48,11 @@ describe('getDefaultBaanxApiBaseUrlForMetaMaskEnv', () => {
5348
const result2 = getDefaultBaanxApiBaseUrlForMetaMaskEnv('production');
5449
expect(result1).toBe(result2);
5550
});
56-
57-
it('uses metaMaskEnv when E2E is true (E2E path)', () => {
58-
process.env.GITHUB_ACTIONS = 'true';
59-
process.env.E2E = 'true';
60-
process.env.BAANX_API_URL = 'https://build-time.api';
61-
expect(getDefaultBaanxApiBaseUrlForMetaMaskEnv('production')).toBe(
62-
AppConstants.BAANX_API_URL.PRD,
63-
);
64-
expect(getDefaultBaanxApiBaseUrlForMetaMaskEnv('dev')).toBe(
65-
AppConstants.BAANX_API_URL.DEV,
66-
);
67-
});
6851
});
6952

70-
describe('when not GITHUB_ACTIONS (Bitrise / .js.env path)', () => {
53+
describe('when not BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY (Bitrise / .js.env path)', () => {
7154
beforeEach(() => {
72-
delete process.env.GITHUB_ACTIONS;
55+
delete process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY;
7356
});
7457

7558
it('returns AppConstants.BAANX_API_URL.PRD for production/rc', () => {

app/components/UI/Card/util/mapBaanxApiUrl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import AppConstants from '../../../../core/AppConstants';
22

33
/**
44
* Returns the Baanx API base URL for the given MetaMask environment.
5-
* When GITHUB_ACTIONS (and not E2E), uses process.env.BAANX_API_URL (set by builds.yml).
5+
* When BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY (and not E2E), uses process.env.BAANX_API_URL (set by builds.yml).
66
* When not (Bitrise / .js.env / E2E), uses AppConstants.BAANX_API_URL per env.
77
*/
88
export const getDefaultBaanxApiBaseUrlForMetaMaskEnv = (
99
metaMaskEnv: string | undefined,
1010
): string => {
11-
if (process.env.GITHUB_ACTIONS === 'true' && process.env.E2E !== 'true') {
11+
if (process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY === 'true') {
1212
return process.env.BAANX_API_URL as string;
1313
}
1414
switch (metaMaskEnv) {

app/components/UI/Ramp/Aggregator/sdk/getSdkEnvironment.test.ts

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,35 @@ import { getSdkEnvironment } from './getSdkEnvironment';
33

44
describe('getSdkEnvironment', () => {
55
const originalProcessEnv = process.env;
6-
const originalGithubActions = process.env.GITHUB_ACTIONS;
6+
const originalBuildsEnabled =
7+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY;
78
const originalRampsEnvironment = process.env.RAMPS_ENVIRONMENT;
8-
const originalE2e = process.env.E2E;
99

1010
beforeEach(() => {
11-
process.env.GITHUB_ACTIONS = 'false';
11+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY = 'false';
1212
});
1313

1414
afterAll(() => {
1515
process.env = originalProcessEnv;
1616
});
1717

1818
afterEach(() => {
19-
if (originalGithubActions !== undefined) {
20-
process.env.GITHUB_ACTIONS = originalGithubActions;
19+
if (originalBuildsEnabled !== undefined) {
20+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY =
21+
originalBuildsEnabled;
2122
} else {
22-
delete process.env.GITHUB_ACTIONS;
23+
delete process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY;
2324
}
2425
if (originalRampsEnvironment !== undefined) {
2526
process.env.RAMPS_ENVIRONMENT = originalRampsEnvironment;
2627
} else {
2728
delete process.env.RAMPS_ENVIRONMENT;
2829
}
29-
if (originalE2e !== undefined) {
30-
process.env.E2E = originalE2e;
31-
} else {
32-
delete process.env.E2E;
33-
}
3430
});
3531

36-
describe('when GITHUB_ACTIONS (builds.yml path)', () => {
32+
describe('when BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY (builds.yml path)', () => {
3733
beforeEach(() => {
38-
process.env.GITHUB_ACTIONS = 'true';
39-
delete process.env.E2E;
34+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY = 'true';
4035
});
4136

4237
it('returns Production when RAMPS_ENVIRONMENT is production', () => {
@@ -59,14 +54,6 @@ describe('getSdkEnvironment', () => {
5954
process.env.RAMPS_ENVIRONMENT = 'staging';
6055
expect(getSdkEnvironment()).toBe(Environment.Staging);
6156
});
62-
63-
it('uses METAMASK_ENVIRONMENT when E2E is true (E2E path)', () => {
64-
process.env.GITHUB_ACTIONS = 'true';
65-
process.env.E2E = 'true';
66-
process.env.RAMPS_ENVIRONMENT = 'staging';
67-
process.env.METAMASK_ENVIRONMENT = 'production';
68-
expect(getSdkEnvironment()).toBe(Environment.Production);
69-
});
7057
});
7158

7259
describe('Production environments', () => {

app/components/UI/Ramp/Aggregator/sdk/getSdkEnvironment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Environment } from '@consensys/on-ramp-sdk';
22

33
/**
4-
* When GITHUB_ACTIONS (and not E2E), uses RAMPS_ENVIRONMENT (set by builds.yml).
4+
* When BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY (and not E2E), uses RAMPS_ENVIRONMENT (set by builds.yml).
55
* When not (Bitrise / .js.env / E2E), uses METAMASK_ENVIRONMENT switch.
66
*/
77
export function getSdkEnvironment() {
8-
if (process.env.GITHUB_ACTIONS === 'true' && process.env.E2E !== 'true') {
8+
if (process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY === 'true') {
99
const rampsEnv = process.env.RAMPS_ENVIRONMENT;
1010
return rampsEnv === 'production'
1111
? Environment.Production

app/components/UI/Ramp/Deposit/sdk/getSdkEnvironment.test.ts

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,32 @@ import { getSdkEnvironment } from './getSdkEnvironment';
33

44
describe('getSdkEnvironment', () => {
55
const originalEnv = process.env.METAMASK_ENVIRONMENT;
6-
const originalGithubActions = process.env.GITHUB_ACTIONS;
6+
const originalBuildsEnabled =
7+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY;
78
const originalRampsEnvironment = process.env.RAMPS_ENVIRONMENT;
8-
const originalE2e = process.env.E2E;
99

1010
beforeEach(() => {
11-
process.env.GITHUB_ACTIONS = 'false';
11+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY = 'false';
1212
});
1313

1414
afterEach(() => {
1515
process.env.METAMASK_ENVIRONMENT = originalEnv;
16-
if (originalGithubActions !== undefined) {
17-
process.env.GITHUB_ACTIONS = originalGithubActions;
16+
if (originalBuildsEnabled !== undefined) {
17+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY =
18+
originalBuildsEnabled;
1819
} else {
19-
delete process.env.GITHUB_ACTIONS;
20+
delete process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY;
2021
}
2122
if (originalRampsEnvironment !== undefined) {
2223
process.env.RAMPS_ENVIRONMENT = originalRampsEnvironment;
2324
} else {
2425
delete process.env.RAMPS_ENVIRONMENT;
2526
}
26-
if (originalE2e !== undefined) {
27-
process.env.E2E = originalE2e;
28-
} else {
29-
delete process.env.E2E;
30-
}
3127
});
3228

33-
describe('when GITHUB_ACTIONS (builds.yml path)', () => {
29+
describe('when BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY (builds.yml path)', () => {
3430
beforeEach(() => {
35-
process.env.GITHUB_ACTIONS = 'true';
36-
delete process.env.E2E;
31+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY = 'true';
3732
});
3833

3934
it('returns Production when RAMPS_ENVIRONMENT is production', () => {
@@ -56,14 +51,6 @@ describe('getSdkEnvironment', () => {
5651
process.env.RAMPS_ENVIRONMENT = 'staging';
5752
expect(getSdkEnvironment()).toBe(SdkEnvironment.Staging);
5853
});
59-
60-
it('uses METAMASK_ENVIRONMENT when E2E is true (E2E path)', () => {
61-
process.env.GITHUB_ACTIONS = 'true';
62-
process.env.E2E = 'true';
63-
process.env.RAMPS_ENVIRONMENT = 'staging';
64-
process.env.METAMASK_ENVIRONMENT = 'production';
65-
expect(getSdkEnvironment()).toBe(SdkEnvironment.Production);
66-
});
6754
});
6855

6956
describe('Production Environment', () => {

app/components/UI/Ramp/Deposit/sdk/getSdkEnvironment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { SdkEnvironment } from '@consensys/native-ramps-sdk';
22

33
/**
4-
* When GITHUB_ACTIONS (and not E2E), uses RAMPS_ENVIRONMENT (set by builds.yml).
4+
* When BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY (and not E2E), uses RAMPS_ENVIRONMENT (set by builds.yml).
55
* When not (Bitrise / .js.env / E2E), uses METAMASK_ENVIRONMENT switch.
66
*/
77
export function getSdkEnvironment() {
8-
if (process.env.GITHUB_ACTIONS === 'true' && process.env.E2E !== 'true') {
8+
if (process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY === 'true') {
99
const rampsEnv = process.env.RAMPS_ENVIRONMENT;
1010
return rampsEnv === 'production'
1111
? SdkEnvironment.Production

app/core/Engine/controllers/ramps-controller/ramps-service-init.test.ts

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,37 +27,32 @@ jest.mock('@metamask/ramps-controller', () => {
2727

2828
describe('getRampsEnvironment', () => {
2929
const originalEnv = process.env.METAMASK_ENVIRONMENT;
30-
const originalGithubActions = process.env.GITHUB_ACTIONS;
30+
const originalBuildsEnabled =
31+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY;
3132
const originalRampsEnvironment = process.env.RAMPS_ENVIRONMENT;
32-
const originalE2e = process.env.E2E;
3333

3434
beforeEach(() => {
35-
process.env.GITHUB_ACTIONS = 'false';
35+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY = 'false';
3636
});
3737

3838
afterEach(() => {
3939
process.env.METAMASK_ENVIRONMENT = originalEnv;
40-
if (originalGithubActions !== undefined) {
41-
process.env.GITHUB_ACTIONS = originalGithubActions;
40+
if (originalBuildsEnabled !== undefined) {
41+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY =
42+
originalBuildsEnabled;
4243
} else {
43-
delete process.env.GITHUB_ACTIONS;
44+
delete process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY;
4445
}
4546
if (originalRampsEnvironment !== undefined) {
4647
process.env.RAMPS_ENVIRONMENT = originalRampsEnvironment;
4748
} else {
4849
delete process.env.RAMPS_ENVIRONMENT;
4950
}
50-
if (originalE2e !== undefined) {
51-
process.env.E2E = originalE2e;
52-
} else {
53-
delete process.env.E2E;
54-
}
5551
});
5652

57-
describe('when GITHUB_ACTIONS (builds.yml path)', () => {
53+
describe('when BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY (builds.yml path)', () => {
5854
beforeEach(() => {
59-
process.env.GITHUB_ACTIONS = 'true';
60-
delete process.env.E2E;
55+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY = 'true';
6156
});
6257

6358
it('returns Production when RAMPS_ENVIRONMENT is production', () => {
@@ -80,14 +75,6 @@ describe('getRampsEnvironment', () => {
8075
process.env.RAMPS_ENVIRONMENT = 'staging';
8176
expect(getRampsEnvironment()).toBe(RampsEnvironment.Staging);
8277
});
83-
84-
it('uses METAMASK_ENVIRONMENT when E2E is true (E2E path)', () => {
85-
process.env.GITHUB_ACTIONS = 'true';
86-
process.env.E2E = 'true';
87-
process.env.RAMPS_ENVIRONMENT = 'staging';
88-
process.env.METAMASK_ENVIRONMENT = 'production';
89-
expect(getRampsEnvironment()).toBe(RampsEnvironment.Production);
90-
});
9178
});
9279

9380
describe('Production Environment', () => {
@@ -167,13 +154,13 @@ describe('rampsServiceInit', () => {
167154
>;
168155
const originalEnv = process.env.METAMASK_ENVIRONMENT;
169156
const originalOS = Platform.OS;
170-
const originalGithubActions = process.env.GITHUB_ACTIONS;
157+
const originalBuildsEnabled =
158+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY;
171159
const originalRampsEnvironment = process.env.RAMPS_ENVIRONMENT;
172-
const originalE2e = process.env.E2E;
173160

174161
beforeEach(() => {
175162
jest.resetAllMocks();
176-
process.env.GITHUB_ACTIONS = 'false';
163+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY = 'false';
177164
const baseControllerMessenger = new ExtendedMessenger<MockAnyNamespace>({
178165
namespace: MOCK_ANY_NAMESPACE,
179166
});
@@ -183,21 +170,17 @@ describe('rampsServiceInit', () => {
183170
afterEach(() => {
184171
process.env.METAMASK_ENVIRONMENT = originalEnv;
185172
Platform.OS = originalOS;
186-
if (originalGithubActions !== undefined) {
187-
process.env.GITHUB_ACTIONS = originalGithubActions;
173+
if (originalBuildsEnabled !== undefined) {
174+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY =
175+
originalBuildsEnabled;
188176
} else {
189-
delete process.env.GITHUB_ACTIONS;
177+
delete process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY;
190178
}
191179
if (originalRampsEnvironment !== undefined) {
192180
process.env.RAMPS_ENVIRONMENT = originalRampsEnvironment;
193181
} else {
194182
delete process.env.RAMPS_ENVIRONMENT;
195183
}
196-
if (originalE2e !== undefined) {
197-
process.env.E2E = originalE2e;
198-
} else {
199-
delete process.env.E2E;
200-
}
201184
});
202185

203186
it('returns service instance', () => {
@@ -305,9 +288,9 @@ describe('rampsServiceInit', () => {
305288
});
306289
});
307290

308-
describe('when GITHUB_ACTIONS (builds.yml path)', () => {
291+
describe('when BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY (builds.yml path)', () => {
309292
beforeEach(() => {
310-
process.env.GITHUB_ACTIONS = 'true';
293+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY = 'true';
311294
delete process.env.E2E;
312295
});
313296

app/core/Engine/controllers/ramps-controller/ramps-service-init.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import {
77
} from '@metamask/ramps-controller';
88

99
/**
10-
* When GITHUB_ACTIONS (and not E2E), uses RAMPS_ENVIRONMENT (set by builds.yml).
10+
* When BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY (and not E2E), uses RAMPS_ENVIRONMENT (set by builds.yml).
1111
* When not (Bitrise / .js.env / E2E), uses METAMASK_ENVIRONMENT switch.
1212
*/
1313
export function getRampsEnvironment(): RampsEnvironment {
14-
if (process.env.GITHUB_ACTIONS === 'true' && process.env.E2E !== 'true') {
14+
if (process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY === 'true') {
1515
const rampsEnv = process.env.RAMPS_ENVIRONMENT;
1616
return rampsEnv === 'production'
1717
? RampsEnvironment.Production

app/core/Engine/controllers/rewards-controller/services/rewards-data-service.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ describe('RewardsDataService', () => {
7676
beforeEach(() => {
7777
jest.clearAllMocks();
7878
process.env = { ...originalEnv };
79-
process.env.GITHUB_ACTIONS = 'false';
79+
process.env.BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY = 'false';
8080

8181
// Allow env overrides by default (canChange = true).
8282
// getRewardsEnvUrl reads canChange from the tuple, so the second element

0 commit comments

Comments
 (0)