Unverified Commit 13590d34 authored by Yue Yang's avatar Yue Yang Committed by GitHub
Browse files

ci: run different jobs depending on the modified files (#2696)


* ci: add ui related tests
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* fix: ci
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* fix(ci): cache-dependency-path
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* fix(ci): order of steps
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* chore: update
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* chore: update
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* chore: remove unused
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* fix: sync ci workflow
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* fix: ci
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* chore: update
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* ci: merge jobs by dorny/paths-filter@v2
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* fix: typo
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* chore: update
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* chore: update
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* fix: cache key
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* chore: remove verify in ci/ui
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* fix: re-add push event in codecov
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* fix: makefile to Makefile
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>

* fix: supplement to the previous commit
Signed-off-by: default avatarYue Yang <g1enyy0ung@gmail.com>
Showing with 120 additions and 340 deletions
+120 -340
name: Check Markdown links
on:
push:
pull_request:
branches:
- master
paths:
- "**.md"
jobs:
markdown-link-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: 'yes'
config-file: '.github/workflows/checklink_config.json'
- uses: actions/checkout@master
- uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: "yes"
config-file: ".github/workflows/checklink_config.json"
# This workflow defines the go and ui related jobs.
#
# We will use the `job` matrix variable to run each subjob. Currently we have:
#
# - verify
# - build
# - test
name: ci
on:
......@@ -7,24 +14,41 @@ on:
- release-*
jobs:
pull:
# JOB to run change detection
changes:
runs-on: ubuntu-latest
# Set job outputs to values from filter step
outputs:
go: ${{ steps.filter.outputs.go }}
ui: ${{ steps.filter.outputs.ui }}
steps:
# For pull requests it's not necessary to checkout the code
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
go:
- Makefile
- go.*
- '**.go'
ui:
- 'ui/**'
- '!ui/README.md'
- '!ui/.*'
go:
needs: changes
if: ${{ needs.changes.outputs.go == 'true' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job:
- verify
- build
- test
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/checkout@v2
- name: Build Chaos Mesh Build Env
if: ${{ github.event.pull_request }}
env:
IMAGE_BUILD_ENV_BUILD: ${{ contains(github.event.pull_request.labels.*.name, 'rebuild-build-env-image') }}
run: |
......@@ -37,7 +61,6 @@ jobs:
make image-build-env
- name: Build Chaos Mesh Dev Env
if: ${{ github.event.pull_request }}
env:
IMAGE_DEV_ENV_BUILD: ${{ contains(github.event.pull_request.labels.*.name, 'rebuild-dev-env-image') }}
run: |
......@@ -50,6 +73,8 @@ jobs:
make image-dev-env
- name: ${{ matrix.job }}
env:
job: ${{ matrix.job }}
run: |
if [[ "$job" == "verify" ]]; then
make check
......@@ -63,5 +88,45 @@ jobs:
else
make $job
fi
env:
job: ${{ matrix.job }}
ui:
needs: changes
if: ${{ needs.changes.outputs.ui == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./ui
strategy:
matrix:
job:
- build
- test
node-version: ["14"] # TODO: add nodejs-16?
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
cache-dependency-path: "ui/yarn.lock"
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Cache app build
if: ${{ matrix.job == 'build' }}
uses: actions/cache@v2
with:
path: ./ui/app/build
key: ${{ runner.os }}-yarn-${{ hashFiles('ui/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Try build
if: ${{ matrix.job == 'build' }}
run: yarn build
- name: Run UT
if: ${{ matrix.job == 'test' }}
run: |
yarn workspace @ui/mui-extends build
yarn workspace @ui/app test
name: Unit Test And Code Coverage
# this workflow would work on all the branches and all the prs
# this workflow would work on all the prs
on:
push: {}
pull_request: {}
pull_request:
paths:
- Makefile
- go.*
- "**.go"
push:
paths:
- Makefile
- go.*
- "**.go"
jobs:
unitTestAndCodeCoverage:
......
name: Integration Test
on:
push:
branches:
- master
pull_request:
branches:
- master
......@@ -26,22 +23,22 @@ jobs:
IMAGE_BUILD_ENV_BUILD: ${{ contains(github.event.pull_request.labels.*.name, 'rebuild-build-env-image') }}
run: |
if [ "${IMAGE_BUILD_ENV_BUILD}" = "true" ] ; then
export IMAGE_BUILD_ENV_BUILD=1;
else
export IMAGE_BUILD_ENV_BUILD=0;
export IMAGE_BUILD_ENV_BUILD=1;
else
export IMAGE_BUILD_ENV_BUILD=0;
fi
make image-build-env
- name: Build Chaos Mesh Dev Env
if: ${{ github.event.pull_request }}
env:
IMAGE_DEV_ENV_BUILD: ${{ contains(github.event.pull_request.labels.*.name, 'rebuild-dev-env-image') }}
run: |
if [ "${IMAGE_DEV_ENV_BUILD}" = "true" ] ; then
export IMAGE_DEV_ENV_BUILD=1;
else
export IMAGE_DEV_ENV_BUILD=0;
export IMAGE_DEV_ENV_BUILD=1;
else
export IMAGE_DEV_ENV_BUILD=0;
fi
make image-dev-env
......
name: License checker
on:
push:
branches:
- master
pull_request:
branches:
- master
......
name: Script Test
name: Test install.sh
on:
push:
branches:
- master
pull_request:
branches:
- master
paths:
- 'install.sh'
jobs:
run:
name: Script Test
runs-on: ubuntu-latest
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-script-test') }}
steps:
......
......@@ -27,7 +27,4 @@ package-lock.json
# eslint
.eslintcache
# ts-interface-builder
*.type-ti.ts
/packages/crd
#!/usr/bin/env bash
# Copyright 2021 Chaos Mesh Authors.
#
# 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
#
# http://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.
#
# Don't run this script directly, use `yarn test:init` to exec it.
SRC_API=src/api/
cd $SRC_API
`npm bin`/ts-interface-builder archives.type.ts --inline-imports
`npm bin`/ts-interface-builder common.type.ts --inline-imports
`npm bin`/ts-interface-builder events.type.ts --inline-imports
`npm bin`/ts-interface-builder experiments.type.ts --inline-imports
# hack
# FIXME: support filter unused types in ts-interface-builder
EXPERIMENT_TYPE_LINE_NUMBER=$(cat experiments.type-ti.ts | grep -m 1 -n -w Experiment | cut -d : -f 1)
sed -i '' -e "$EXPERIMENT_TYPE_LINE_NUMBER,+5d" experiments.type-ti.ts
EXPERIMENT_TYPE_LINE_NUMBER=$(cat experiments.type-ti.ts | grep -n -w Experiment | tail -n 2 | head -n 1 | cut -d : -f 1)
sed -i '' -e "${EXPERIMENT_TYPE_LINE_NUMBER}d" experiments.type-ti.ts
cd -
......@@ -50,7 +50,6 @@
"build": "react-scripts build",
"analyze": "source-map-explorer 'build/static/js/*.js' --only-mapped",
"test": "react-scripts test",
"test:init": "./__tests__/interface-builder.sh",
"eject": "react-scripts eject"
},
"eslintConfig": {
......@@ -94,9 +93,7 @@
"cross-env": "^7.0.3",
"http-proxy-middleware": "^2.0.1",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.2",
"ts-interface-builder": "^0.3.2",
"ts-interface-checker": "^1.0.2"
"source-map-explorer": "^2.5.2"
},
"jest": {
"resetMocks": false
......
/*
* Copyright 2021 Chaos Mesh Authors.
*
* 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
*
* http://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.
*
*/
import ArchiveTI from 'api/archives.type-ti'
import { createCheckers } from 'ts-interface-checker'
const { Archive } = createCheckers(ArchiveTI)
const dummyArchive = {
uid: 'xxx',
kind: 'PodChaos',
namespace: 'default',
name: 'pod-kill',
start_time: 'xxx',
finish_time: 'xxx',
}
describe('Check archives type', () => {
it('Archive', () => {
// Normal
Archive.check(dummyArchive)
// Abnormal
expect(() => Archive.check({ ...dummyArchive, kind: 'HelloWorldChaos' })).toThrow(
'value.kind is not a ExperimentKind'
)
expect(() => Archive.check({ ...dummyArchive, namespace: null })).toThrow('value.namespace is not a string')
expect(() => Archive.check({ ...dummyArchive, name: null })).toThrow('value.name is not a string')
expect(() => Archive.check({ ...dummyArchive, start_time: null })).toThrow('value.start_time is not a string')
expect(() => Archive.check({ ...dummyArchive, finish_time: null })).toThrow('value.finish_time is not a string')
})
})
/*
* Copyright 2021 Chaos Mesh Authors.
*
* 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
*
* http://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.
*
*/
import CommonTI from 'api/common.type-ti'
import { createCheckers } from 'ts-interface-checker'
const { Config, RBACConfigParams } = createCheckers(CommonTI)
const dummyConfig = {
security_mode: true,
dns_server_create: false,
gcp_security_mode: false,
version: 'xxx',
}
const dummyRBACConfigParams = {
namespace: 'xxx',
role: 'viewer',
}
describe('Check common type', () => {
it('Config', () => {
// Normal
Config.strictCheck(dummyConfig)
})
it('RBACConfigParams', () => {
// Normal
RBACConfigParams.check(dummyRBACConfigParams)
// Abnormal
expect(() => RBACConfigParams.check({ ...dummyRBACConfigParams, role: 'xxx' })).toThrow(
'value.role is none of "manager", "viewer"'
)
})
})
/*
* Copyright 2021 Chaos Mesh Authors.
*
* 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
*
* http://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.
*
*/
import EventTI from 'api/events.type-ti'
import { createCheckers } from 'ts-interface-checker'
const { Event, EventPod } = createCheckers(EventTI)
const dummyEvent = {
id: 1,
experiment_id: 'xxx',
experiment: 'xxx',
namespace: 'default',
kind: 'PodChaos',
message: 'xxx',
start_time: 'xxx',
finish_time: 'xxx',
}
const dummyPod = {
id: 1,
pod_ip: 'xxx',
pod_name: 'xxx',
namespace: 'xxx',
action: 'xxx',
message: 'xxx',
}
describe('Check events type', () => {
it('Event', () => {
// Normal
Event.check(dummyEvent)
// Abnormal
expect(() => Event.check({ ...dummyEvent, experiment: null })).toThrow('value.experiment is not a string')
expect(() => Event.check({ ...dummyEvent, namespace: null })).toThrow('value.namespace is not a string')
expect(() => Event.check({ ...dummyEvent, kind: 'HelloWorldChaos' })).toThrow('value.kind is not a ExperimentKind')
expect(() => Event.check({ ...dummyEvent, message: null })).toThrow('value.message is not a string')
expect(() => Event.check({ ...dummyEvent, start_time: null })).toThrow('value.start_time is not a string')
expect(() => Event.check({ ...dummyEvent, finish_time: null })).toThrow('value.finish_time is not a string')
})
it('EventPod', () => {
// Normal
EventPod.strictCheck(dummyPod)
})
})
/*
* Copyright 2021 Chaos Mesh Authors.
*
* 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
*
* http://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.
*
*/
import ExperimentTI from 'api/experiments.type-ti'
import { createCheckers } from 'ts-interface-checker'
const { Experiment } = createCheckers(ExperimentTI)
const dummyExperiment = {
uid: 'xxx',
kind: 'PodChaos',
namespace: 'default',
name: 'pod-kill',
created: 'xxx',
status: 'Running',
}
describe('Check experiments type', () => {
it('Experiment', () => {
// Normal
Experiment.check(dummyExperiment)
Experiment.check({ ...dummyExperiment, status: 'Waiting' })
// Abnormal
expect(() => Experiment.check({ ...dummyExperiment, kind: 'HelloWorldChaos' })).toThrow(
'value.kind is not a ExperimentKind'
)
expect(() => Experiment.check({ ...dummyExperiment, namespace: null })).toThrow('value.namespace is not a string')
expect(() => Experiment.check({ ...dummyExperiment, name: null })).toThrow('value.name is not a string')
expect(() => Experiment.check({ ...dummyExperiment, created: null })).toThrow('value.created is not a string')
expect(() => Experiment.check({ ...dummyExperiment, status: 'Unknown' })).toThrow(
'value.status is none of "Running", "Waiting", "Paused", "Failed", "Finished"'
)
})
})
......@@ -31,7 +31,6 @@ export interface Experiment {
namespace: string
name: string
created_at: string
// FIXME: support keyof in ts-interface-builder
status: 'injecting' | 'running' | 'finished' | 'paused' | 'deleting'
}
......
......@@ -39,6 +39,7 @@ test('sanitize an object where all values are empty', () => {
c: null,
d: undefined,
e: [],
f: {},
})
).toEqual({})
})
......@@ -72,5 +72,5 @@ export function sanitize(obj: any) {
return false
}
return JSON.parse(JSON.stringify(obj, (_, value: any) => (isEmpty(value) ? undefined : value)))
return JSON.parse(JSON.stringify(obj, (_, value: any) => (isEmpty(value) ? undefined : value)) ?? '{}')
}
/*
* Copyright 2021 Chaos Mesh Authors.
*
* 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
*
* http://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.
*
*/
import { BasicType, basicTypes } from 'ts-interface-checker/dist/types'
basicTypes['uuid'] = new BasicType((v) => typeof v === 'string', 'is not a UUID')
......@@ -21,4 +21,3 @@
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom'
import './setupTests.api'
......@@ -11,12 +11,13 @@
"scripts": {
"bootstrap": "./scripts/bootstrap.sh",
"build": "yarn workspace @ui/mui-extends build && yarn workspace @ui/app build",
"prepare": "cd .. && husky install ./ui/.husky",
"prepare": "cd ..; is-ci || husky install ./ui/.husky",
"clean": "rimraf app/node_modules && rimraf packages/**/node_modules && rimraf node_modules"
},
"devDependencies": {
"husky": "^7.0.4",
"import-sort-style-eslint": "^6.0.0",
"is-ci": "^3.0.1",
"lint-staged": "^12.1.2",
"prettier": "^2.5.0",
"prettier-plugin-import-sort": "^0.0.7",
......
......@@ -3834,7 +3834,7 @@ combined-stream@^1.0.8:
dependencies:
delayed-stream "~1.0.0"
commander@2, commander@^2.12.2, commander@^2.20.0:
commander@2, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
......@@ -5727,15 +5727,6 @@ fs-extra@^10.0.0:
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-extra@^4.0.3:
version "4.0.3"
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz"
integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^9.0.0, fs-extra@^9.0.1:
version "9.1.0"
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz"
......@@ -6426,6 +6417,13 @@ is-callable@^1.1.4, is-callable@^1.2.4:
resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz"
integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
is-ci@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867"
integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==
dependencies:
ci-info "^3.2.0"
is-core-module@^2.2.0, is-core-module@^2.8.0:
version "2.8.0"
resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz"
......@@ -7224,13 +7222,6 @@ json5@^2.1.2, json5@^2.2.0:
dependencies:
minimist "^1.2.5"
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz"
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz"
......@@ -10448,21 +10439,6 @@ tryer@^1.0.1:
resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
ts-interface-builder@^0.3.2:
version "0.3.2"
resolved "https://registry.npmjs.org/ts-interface-builder/-/ts-interface-builder-0.3.2.tgz"
integrity sha512-8LcB+qSwnDzBeP47Nug2+4NUjdRNJ94MfzLNXQ4mmAM8UidDDQS0YoD7Ng6XONa8rX6nJenlgph1X459VYqypQ==
dependencies:
commander "^2.12.2"
fs-extra "^4.0.3"
glob "^7.1.6"
typescript "^3.0.0"
ts-interface-checker@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-1.0.2.tgz"
integrity sha512-4IKKvhZRXhvtYF/mtu+OCfBqJKV6LczUq4kQYcpT+iSB7++R9+giWnp2ecwWMIcnG16btVOkXFnoxLSYMN1Q1g==
tsconfig-paths@^3.11.0:
version "3.11.0"
resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz"
......@@ -10544,7 +10520,7 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typescript@^3.0.0, typescript@^3.2.4:
typescript@^3.2.4:
version "3.9.10"
resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz"
integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==
......@@ -10599,7 +10575,7 @@ unique-string@^2.0.0:
dependencies:
crypto-random-string "^2.0.0"
universalify@^0.1.0, universalify@^0.1.2:
universalify@^0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment