-
Notifications
You must be signed in to change notification settings - Fork 2k
migrate code from googleapis/nodejs-security-center #2867
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
84 commits
Select commit
Hold shift + click to select a range
4518443
add more scaffolding
jkwlui efbd4b9
refactor: fix lint rules and make consistent (#1)
JustinBeckwith 1bbd671
add dummy no test yet file to system test and samples test dirs
jkwlui 6f1f7a5
Release @google-cloud/security-center v0.1.1 (#28)
jkwlui 532a1d5
samples: add quickstart (#33)
jkwlui c34f74a
Release v0.1.2 (#37)
jkwlui a5c0f98
chore(deps): update dependency mocha to v6
renovate[bot] acb4c2d
Release @google-cloud/security-center v0.2.0 (#64)
JustinBeckwith 39e2c86
Release @google-cloud/security-center v0.3.0 (#68)
JustinBeckwith 84ccb0d
refactor: use execSync for tests
JustinBeckwith d8af503
fix: import run_asset_discovery_response.proto (#119)
yoshi-automation 721ebf6
chore: release 1.0.0 (#120)
yoshi-automation 4310462
docs(samples): add a comprehensive set of samples (#82)
emkornfield 4cfe886
chore: release 2.0.0 (#123)
yoshi-automation b73ac8a
chore: release 2.0.1 (#136)
yoshi-automation 5917352
refactor: drop dependency on execa (#131)
JustinBeckwith b9e5237
chore: release 2.1.0 (#143)
yoshi-automation 7b0c242
chore: release 2.2.1 (#159)
release-please[bot] d23c331
chore: release 2.2.2 (#168)
release-please[bot] 0ed1321
chore: update license headers (#176)
JustinBeckwith 020bd4c
chore: release 2.3.0 (#181)
release-please[bot] d248754
refactor: use explicit mocha imports (#187)
JustinBeckwith bb2959b
chore: release 2.3.1
release-please[bot] 1b7b458
chore(deps): update dependency mocha to v7 (#190)
renovate-bot 3558018
chore: release 2.3.2 (#200)
release-please[bot] 6fd3d61
chore: release 3.0.0 (#204)
release-please[bot] c54115f
chore: release 3.0.1 (#208)
release-please[bot] 00c98db
docs: Add v1p1beta1 notifications samples (#214)
tdh911 7403d01
docs: Fix typo in sample label (#227)
tdh911 bcf0c85
chore: release 3.1.0 (#218)
release-please[bot] ea95500
chore: update to latest version of uuid (#235)
bcoe 4330fae
docs: update notification samples to v1 (#236)
tdh911 a515204
feat!: drop node8 support, support for async iterators (#248)
alexander-fenster 3f30f5f
build: new coverage action (#271)
yoshi-automation b3eb134
chore(deps): update dependency uuid to v8 (#272)
renovate-bot 939511b
fix(deps): update dependency @google-cloud/pubsub to v2 (#276)
renovate-bot 1377e23
chore: release 4.0.0 (#249)
release-please[bot] ccf331e
chore(deps): update dependency mocha to v8 (#282)
renovate-bot c9157a9
chore: release 5.0.0 (#287)
release-please[bot] 424f2b9
chore: release 5.0.1 (#291)
release-please[bot] 20acb12
docs: Add filter field libary sample for UpdateNotificationConfig (#288)
hannah-tsai d2c0fae
chore: release 5.0.2 (#304)
release-please[bot] 5c7ccd6
fix(deps): roll back dependency @google-cloud/security-center to ^5.0…
renovate-bot d5a8577
chore: release 5.0.3 (#310)
release-please[bot] 6a602a9
chore: release 5.1.0 (#318)
release-please[bot] 941253c
fix(deps): roll back dependency @google-cloud/security-center to ^5.0…
renovate-bot 3a4cca1
chore: release 5.1.1 (#320)
release-please[bot] 4275a62
chore: release 5.1.2 (#338)
release-please[bot] 52068c6
chore: release 5.1.3 (#345)
release-please[bot] 5377da3
chore: release 5.2.0 (#350)
release-please[bot] 07e2920
docs: wrap samples with future prefix (#348)
Strykrol 76ef5e0
docs: update new tags to match most used tag from other langs (#358)
8062d54
docs: remove unused region tags (#359)
895d28f
chore: release 5.3.0 (#381)
release-please[bot] a4c2557
chore: release 5.3.1 (#386)
release-please[bot] 3fe0a61
chore: release 5.3.2 (#397)
release-please[bot] b47552e
chore: release 5.3.3 (#402)
release-please[bot] 0112047
chore: release 5.3.4 (#404)
release-please[bot] 97aa779
chore: release 5.3.5 (#406)
release-please[bot] df41b5d
chore: release 5.4.0 (#411)
release-please[bot] ff2c0ca
chore: release 5.4.1 (#416)
release-please[bot] 2c07ebf
chore: release 5.5.0 (#418)
release-please[bot] 5055b10
chore: release 5.5.1 (#421)
release-please[bot] a4e4424
feat: Added vulnerability field to the finding
gcf-owl-bot[bot] 066036f
chore: release 5.6.0 (#430)
release-please[bot] a643e21
chore: release 5.7.0 (#439)
release-please[bot] 9e88d7d
chore: release 5.8.0 (#442)
release-please[bot] e18892d
chore(main): release 5.9.0 (#458)
release-please[bot] b3af98e
chore(main): release 5.10.0 (#471)
release-please[bot] 71312f0
chore(main): release 5.11.0 (#480)
release-please[bot] 018a251
build!: update library to use Node 12 (#486)
sofisl 2e912ce
chore(main): release 6.0.0 (#488)
release-please[bot] ec6261d
chore(deps): update dependency @google-cloud/pubsub to v3 (#489)
renovate-bot 531cb09
chore(main): release 6.1.0 (#496)
release-please[bot] c45f26c
chore(main): release 6.2.0 (#500)
release-please[bot] cddbb86
chore(main): release 6.3.0 (#502)
release-please[bot] 01fbe56
chore(main): release 6.3.1 (#508)
release-please[bot] c27740c
chore(deps): update dependency uuid to v9 (#509)
renovate-bot 00db374
Merge remote-tracking branch 'migration/main' into nodejs-security-ce…
53cf945
add github workflow for security-center
fb09fa0
remove eslintrc
fda4e51
Update .github/workflows/security-center-snippets.yaml
45f41d7
Merge branch 'main' into nodejs-security-center-migration
1a035ee
Update security-center-snippets.yaml
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
name: security-center-snippets | ||
on: | ||
push: | ||
branches: | ||
- main | ||
paths: | ||
- 'security-center/snippets/**' | ||
pull_request: | ||
paths: | ||
- 'security-center/snippets/**' | ||
pull_request_target: | ||
types: [labeled] | ||
paths: | ||
- 'security-center/snippets/**' | ||
schedule: | ||
- cron: '0 0 * * 0' | ||
env: | ||
GCLOUD_ORGANIZATION: 1081635000895 | ||
jobs: | ||
test: | ||
if: ${{ github.event.action != 'labeled' || github.event.label.name == 'actions:force-run' }} | ||
runs-on: ubuntu-latest | ||
timeout-minutes: 60 | ||
permissions: | ||
contents: 'write' | ||
pull-requests: 'write' | ||
id-token: 'write' | ||
steps: | ||
- uses: actions/[email protected] | ||
with: | ||
ref: ${{github.event.pull_request.head.sha}} | ||
- uses: 'google-github-actions/[email protected]' | ||
with: | ||
workload_identity_provider: 'projects/1046198160504/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider' | ||
service_account: '[email protected]' | ||
create_credentials_file: 'true' | ||
access_token_lifetime: 600s | ||
- uses: actions/[email protected] | ||
with: | ||
node-version: 16 | ||
- run: npm install | ||
working-directory: security-center/snippets | ||
- run: npm test | ||
working-directory: security-center/snippets | ||
env: | ||
MOCHA_REPORTER_SUITENAME: security_center_snippets | ||
MOCHA_REPORTER_OUTPUT: security_center_snippets_sponge_log.xml | ||
MOCHA_REPORTER: xunit | ||
- if: ${{ github.event.action == 'labeled' && github.event.label.name == 'actions:force-run' }} | ||
uses: actions/github-script@v6 | ||
with: | ||
github-token: ${{ secrets.GITHUB_TOKEN }} | ||
script: | | ||
try { | ||
await github.rest.issues.removeLabel({ | ||
name: 'actions:force-run', | ||
owner: 'GoogleCloudPlatform', | ||
repo: 'nodejs-docs-samples', | ||
issue_number: context.payload.pull_request.number | ||
}); | ||
} catch (e) { | ||
if (!e.message.includes('Label does not exist')) { | ||
throw e; | ||
} | ||
} | ||
- if: ${{ github.event_name == 'schedule'}} | ||
run: | | ||
curl https://github.com/googleapis/repo-automation-bots/releases/download/flakybot-1.1.0/flakybot -o flakybot -s -L | ||
chmod +x ./flakybot | ||
./flakybot --repo GoogleCloudPlatform/nodejs-docs-samples --commit_hash ${{github.sha}} --build_url https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,8 +57,9 @@ | |
"datacatalog/cloud-client", | ||
"datacatalog/quickstart", | ||
"datastore/functions", | ||
"service-directory/snippets", | ||
"scheduler", | ||
"security-center/snippets", | ||
"service-directory/snippets", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Praise: Thanks for fixing the order here. :) |
||
"secret-manager", | ||
"speech", | ||
"talent", | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{ | ||
"name": "nodejs-security-center-samples", | ||
"private": true, | ||
"files": [ | ||
"**/*.js", | ||
"!system-test/" | ||
], | ||
"engines": { | ||
"node": ">=12.0.0" | ||
}, | ||
"scripts": { | ||
"test": "mocha system-test/ --recursive --timeout 6000000" | ||
}, | ||
"license": "Apache-2.0", | ||
"dependencies": { | ||
"@google-cloud/pubsub": "^3.0.0", | ||
"@google-cloud/security-center": "^6.3.1" | ||
}, | ||
"devDependencies": { | ||
"chai": "^4.2.0", | ||
"mocha": "^8.0.0", | ||
"uuid": "^9.0.0" | ||
} | ||
} |
86 changes: 86 additions & 0 deletions
86
security-center/snippets/system-test/v1/assetSecurityMarks.test.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// Copyright 2019 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// https://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
'use strict'; | ||
|
||
const {SecurityCenterClient} = require('@google-cloud/security-center'); | ||
const {assert} = require('chai'); | ||
const {describe, it, before} = require('mocha'); | ||
const {execSync} = require('child_process'); | ||
const exec = cmd => execSync(cmd, {encoding: 'utf8'}); | ||
|
||
const organizationId = process.env['GCLOUD_ORGANIZATION']; | ||
|
||
describe('client with security marks for assets', async () => { | ||
let data; | ||
before(async () => { | ||
// Creates a new client. | ||
const client = new SecurityCenterClient(); | ||
|
||
const [assetResults] = await client.listAssets({ | ||
parent: client.organizationPath(organizationId), | ||
}); | ||
const randomAsset = | ||
assetResults[Math.floor(Math.random() * assetResults.length)].asset; | ||
console.log('random %j', randomAsset); | ||
data = { | ||
orgId: organizationId, | ||
assetName: randomAsset.name, | ||
}; | ||
console.log('data %j', data); | ||
}); | ||
it('client can add security marks to asset.', () => { | ||
const output = exec(`node v1/addSecurityMarks.js ${data.assetName}`); | ||
assert.include(output, data.assetName); | ||
assert.match(output, /key_a/); | ||
assert.match(output, /value_a/); | ||
assert.match(output, /key_b/); | ||
assert.match(output, /value_b/); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can add and delete security marks', () => { | ||
// Ensure marks are set. | ||
exec(`node v1/addSecurityMarks.js ${data.assetName}`); | ||
|
||
const output = exec(`node v1/addDeleteSecurityMarks.js ${data.assetName}`); | ||
assert.match(output, /key_a/); | ||
assert.match(output, /new_value_a/); | ||
assert.notMatch(output, /key_b/); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can delete security marks', () => { | ||
// Ensure marks are set. | ||
exec(`node v1/addSecurityMarks.js ${data.assetName}`); | ||
|
||
const output = exec(`node v1/deleteSecurityMarks.js ${data.assetName}`); | ||
assert.notMatch(output, /key_a/); | ||
assert.notMatch(output, /value_a/); | ||
assert.notMatch(output, /key_b/); | ||
assert.notMatch(output, /value_b/); | ||
assert.include(output, data.assetName); | ||
assert.include(output, data.assetName); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can list assets with security marks', () => { | ||
// Ensure marks are set. | ||
exec(`node v1/addSecurityMarks.js ${data.assetName}`); | ||
|
||
const output = exec(`node v1/listAssetsWithSecurityMarks.js ${data.orgId}`); | ||
assert.include(output, data.assetName); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
}); |
204 changes: 204 additions & 0 deletions
204
security-center/snippets/system-test/v1/findings.test.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
// Copyright 2019 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// https://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
'use strict'; | ||
|
||
const {SecurityCenterClient} = require('@google-cloud/security-center'); | ||
const {assert} = require('chai'); | ||
const {describe, it, before} = require('mocha'); | ||
const {execSync} = require('child_process'); | ||
const exec = cmd => execSync(cmd, {encoding: 'utf8'}); | ||
|
||
const organizationId = process.env['GCLOUD_ORGANIZATION']; | ||
|
||
describe('Client with SourcesAndFindings', async () => { | ||
let data; | ||
before(async () => { | ||
// Creates a new client. | ||
const client = new SecurityCenterClient(); | ||
const [source] = await client | ||
.createSource({ | ||
source: { | ||
displayName: 'Customized Display Name', | ||
description: 'A new custom source that does X', | ||
}, | ||
parent: client.organizationPath(organizationId), | ||
}) | ||
.catch(error => console.error(error)); | ||
const eventTime = new Date(); | ||
const createFindingTemplate = { | ||
parent: source.name, | ||
findingId: 'somefinding', | ||
finding: { | ||
state: 'ACTIVE', | ||
// Resource the finding is associated with. This is an | ||
// example any resource identifier can be used. | ||
resourceName: | ||
'//cloudresourcemanager.googleapis.com/organizations/11232', | ||
// A free-form category. | ||
category: 'MEDIUM_RISK_ONE', | ||
// The time associated with discovering the issue. | ||
eventTime: { | ||
seconds: Math.floor(eventTime.getTime() / 1000), | ||
nanos: (eventTime.getTime() % 1000) * 1e6, | ||
}, | ||
}, | ||
}; | ||
const [finding] = await client.createFinding(createFindingTemplate); | ||
createFindingTemplate.findingId = 'untouchedFindingId'; | ||
createFindingTemplate.finding.category = 'XSS'; | ||
const [untouchedFinding] = await client | ||
.createFinding(createFindingTemplate) | ||
.catch(error => console.error(error)); | ||
data = { | ||
orgId: organizationId, | ||
sourceName: source.name, | ||
findingName: finding.name, | ||
untouchedFindingName: untouchedFinding.name, | ||
}; | ||
console.log('my data %j', data); | ||
}); | ||
|
||
it('client can create source', () => { | ||
const output = exec(`node v1/createSource.js ${data.orgId}`); | ||
assert.match(output, new RegExp(data.orgId)); | ||
assert.match(output, /New Source/); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can get source', () => { | ||
const output = exec(`node v1/getSource.js ${data.sourceName}`); | ||
assert.match(output, new RegExp(data.sourceName)); | ||
assert.match(output, /Source/); | ||
assert.match(output, /"description":"A new custom source that does X"/); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can list all sources', () => { | ||
const output = exec(`node v1/listAllSources.js ${data.orgId}`); | ||
assert.match(output, new RegExp(data.sourceName)); | ||
assert.match(output, /Sources/); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can update a source', () => { | ||
const output = exec(`node v1/updateSource.js ${data.sourceName}`); | ||
assert.match(output, new RegExp(data.sourceName)); | ||
assert.match(output, /New Display Name/); | ||
assert.match(output, /source that does X/); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can create a finding', () => { | ||
const output = exec(`node v1/createFinding.js ${data.sourceName}`); | ||
assert.match(output, new RegExp(data.sourceName)); | ||
assert.match(output, /New finding created/); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can create a finding with source properties', () => { | ||
const output = exec( | ||
`node v1/createFindingSourceProperties.js ${data.sourceName}` | ||
); | ||
assert.match(output, new RegExp(data.sourceName)); | ||
assert.match(output, /New finding created/); | ||
assert.match(output, /n_value/); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can update a findings source properties', () => { | ||
const output = exec( | ||
`node v1/updateFindingSourceProperties.js ${data.findingName}` | ||
); | ||
assert.match(output, new RegExp(data.findingName)); | ||
assert.match(output, /Updated Finding/); | ||
assert.match(output, /new_string_example/); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can set finding state', () => { | ||
const output = exec(`node v1/setFindingState.js ${data.findingName}`); | ||
assert.match(output, new RegExp(data.findingName)); | ||
assert.match(output, /INACTIVE/); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can test IAM privileges', () => { | ||
const output = exec(`node v1/testIam.js ${data.sourceName}`); | ||
assert.equal( | ||
(output.match(/true/g) || []).length, | ||
2, | ||
`${output} contains true twice` | ||
); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can list all findings', () => { | ||
const output = exec(`node v1/listAllFindings.js ${data.orgId}`); | ||
assert.match(output, new RegExp(data.findingName)); | ||
assert.match(output, new RegExp(data.untouchedFindingName)); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can list only some findings', () => { | ||
const output = exec(`node v1/listFilteredFindings.js ${data.sourceName}`); | ||
assert.match(output, new RegExp(data.findingName)); | ||
assert.notMatch(output, new RegExp(data.untouchedFindingName)); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can list findings at a time.', () => { | ||
const output = exec(`node v1/listFindingsAtTime.js ${data.sourceName}`); | ||
// Nothing was created for the source more then a few minutes ago, so | ||
// days ago should return nothing. | ||
assert.equal(output, ''); | ||
}); | ||
|
||
it('client can add security marks to finding', () => { | ||
const output = exec( | ||
`node v1/addFindingSecurityMarks.js ${data.findingName}` | ||
); | ||
assert.match(output, new RegExp(data.findingName)); | ||
assert.match(output, /key_a/); | ||
assert.match(output, /value_a/); | ||
assert.match(output, /key_b/); | ||
assert.match(output, /value_b/); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can list findings withe security marks', () => { | ||
// Ensure marks are set. | ||
exec(`node v1/addFindingSecurityMarks.js ${data.findingName}`); | ||
const output = exec( | ||
`node v1/listFindingsWithSecurityMarks.js ${data.sourceName}` | ||
); | ||
assert.notMatch(output, new RegExp(data.findingName)); | ||
assert.match(output, new RegExp(data.untouchedFindingName)); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client can get a sources policy', () => { | ||
const output = exec(`node v1/getSourceIam.js ${data.sourceName}`); | ||
assert.match(output, /Current policy/); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
|
||
it('client set a sources policy', () => { | ||
const user = '[email protected]'; | ||
const output = exec(`node v1/setSourceIam.js ${data.sourceName} ${user}`); | ||
assert.match(output, /Updated policy/); | ||
assert.include(output, user); | ||
assert.notMatch(output, /undefined/); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.