Skip to content

Commit da4b206

Browse files
pjcdawkinsclaudeCopilot
authored
chore(legacy): subtree merge legacy-cli 5.x updates (#19)
* chore(legacy): subtree merge legacy-cli 5.x updates Merge 31 commits from platformsh/legacy-cli 5.x (adb14ee..08be158). Notable upstream changes: - Autoscaling settings: expanded get/set commands - Variable app-scope: new --app-scope option for var:create/update - CurlCli: stdout buffering for 401 retry - Environment push: additional options - Db dump: new options - Various config and metadata updates Conflicts resolved: - legacy/src/Service/CurlCli.php: took upstream changes, fixed stdout double-write bug in retry block - legacy/go-tests/: removed (moved to integration-tests/ in this repo) Additional changes in this repo: - Port variable app-scope tests to integration-tests/ with setupVariableTest helper and two new test functions - Add ApplicationScope field to mockapi Variable struct - Remove legacy Makefile go-tests targets (now in integration-tests/) - Fix data race in api_curl_test with tokenState struct - Suppress phpstan false positive in CurlCli.php Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update legacy/src/Service/Relationships.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(legacy): address Copilot review on subtree merge - DbDumpCommand: invert --no-tablespaces condition so the flag is applied for oracle-mysql (where mysqldump lacks the PROCESS privilege to dump tablespaces). The upstream fix in platformsh/legacy-cli#1593 has the inverse condition: it adds the flag for non-Oracle DBs, leaving oracle-mysql still failing with "Access denied; you need (at least one of) the PROCESS privilege(s) for this operation". This local fix should also be sent upstream. - VariableCommandUtil::listApps: guard against a null default_branch before calling Api::getEnvironment(string), which would otherwise TypeError. Matches the existing null check in DomainCommandBase. - AutoscalingSettingsGet/Set: update AsCommand descriptions to mention services. Upstream PR #1587 added service-level autoscaling but left the help text saying "apps and workers" only. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 2dc4f49 commit da4b206

23 files changed

Lines changed: 467 additions & 148 deletions

integration-tests/api_curl_test.go

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,49 @@ import (
66
"net/http/httptest"
77
"os/exec"
88
"strings"
9+
"sync"
910
"testing"
1011

1112
"github.com/go-chi/chi/v5"
1213
"github.com/go-chi/chi/v5/middleware"
1314
"github.com/stretchr/testify/assert"
1415
)
1516

17+
// tokenState holds mutable token state shared between the test and HTTP handlers.
18+
type tokenState struct {
19+
mu sync.Mutex
20+
validToken string
21+
tokenFetches int
22+
}
23+
24+
func (s *tokenState) setToken(token string) {
25+
s.mu.Lock()
26+
defer s.mu.Unlock()
27+
s.validToken = token
28+
}
29+
30+
func (s *tokenState) getToken() string {
31+
s.mu.Lock()
32+
defer s.mu.Unlock()
33+
return s.validToken
34+
}
35+
36+
// fetchToken increments the fetch count and returns the current valid token.
37+
func (s *tokenState) fetchToken() string {
38+
s.mu.Lock()
39+
defer s.mu.Unlock()
40+
s.tokenFetches++
41+
return s.validToken
42+
}
43+
44+
func (s *tokenState) getFetches() int {
45+
s.mu.Lock()
46+
defer s.mu.Unlock()
47+
return s.tokenFetches
48+
}
49+
1650
func TestApiCurlCommand(t *testing.T) {
17-
validToken := "valid-token"
51+
state := &tokenState{validToken: "valid-token"}
1852

1953
mux := chi.NewMux()
2054
if testing.Verbose() {
@@ -23,7 +57,7 @@ func TestApiCurlCommand(t *testing.T) {
2357
mux.Use(func(next http.Handler) http.Handler {
2458
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2559
if !strings.HasPrefix(r.URL.Path, "/oauth2") {
26-
if r.Header.Get("Authorization") != "Bearer "+validToken {
60+
if r.Header.Get("Authorization") != "Bearer "+state.getToken() {
2761
w.WriteHeader(http.StatusUnauthorized)
2862
//nolint:lll
2963
_ = json.NewEncoder(w).Encode(map[string]any{"error": "invalid_token", "error_description": "Invalid access token."})
@@ -33,11 +67,10 @@ func TestApiCurlCommand(t *testing.T) {
3367
next.ServeHTTP(w, r)
3468
})
3569
})
36-
var tokenFetches int
3770
mux.Post("/oauth2/token", func(w http.ResponseWriter, _ *http.Request) {
71+
tok := state.fetchToken()
3872
w.WriteHeader(http.StatusOK)
39-
tokenFetches++
40-
_ = json.NewEncoder(w).Encode(map[string]any{"access_token": validToken, "expires_in": 900, "token_type": "bearer"})
73+
_ = json.NewEncoder(w).Encode(map[string]any{"access_token": tok, "expires_in": 900, "token_type": "bearer"})
4174
})
4275
mux.Get("/users/me", func(w http.ResponseWriter, _ *http.Request) {
4376
_ = json.NewEncoder(w).Encode(map[string]any{"id": "userID", "email": "me@example.com"})
@@ -52,24 +85,24 @@ func TestApiCurlCommand(t *testing.T) {
5285

5386
// Load the first token.
5487
assert.Equal(t, "success", f.Run("api:curl", "/fake-api-path"))
55-
assert.Equal(t, 1, tokenFetches)
88+
assert.Equal(t, 1, state.getFetches())
5689

5790
// Revoke the access token and try the command again.
5891
// The old token should be considered invalid, so the API call should return 401,
5992
// and then the CLI should refresh the token and retry.
60-
validToken = "new-valid-token"
93+
state.setToken("new-valid-token")
6194
assert.Equal(t, "success", f.Run("api:curl", "/fake-api-path"))
62-
assert.Equal(t, 2, tokenFetches)
95+
assert.Equal(t, 2, state.getFetches())
6396

6497
assert.Equal(t, "success", f.Run("api:curl", "/fake-api-path"))
65-
assert.Equal(t, 2, tokenFetches)
98+
assert.Equal(t, 2, state.getFetches())
6699

67100
// If --no-retry-401 and --fail are provided then the command should return exit code 22.
68-
validToken = "another-new-valid-token"
101+
state.setToken("another-new-valid-token")
69102
stdOut, _, err := f.RunCombinedOutput("api:curl", "/fake-api-path", "--no-retry-401", "--fail")
70103
exitErr := &exec.ExitError{}
71104
assert.ErrorAs(t, err, &exitErr)
72105
assert.Equal(t, 22, exitErr.ExitCode())
73106
assert.Empty(t, stdOut)
74-
assert.Equal(t, 2, tokenFetches)
107+
assert.Equal(t, 2, state.getFetches())
75108
}

integration-tests/variable_write_test.go

Lines changed: 116 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,66 +9,154 @@ import (
99
"github.com/upsun/cli/pkg/mockapi"
1010
)
1111

12-
func TestVariableCreate(t *testing.T) {
12+
// variableTestSetup holds common test infrastructure for variable tests.
13+
type variableTestSetup struct {
14+
authServer *httptest.Server
15+
apiServer *httptest.Server
16+
apiHandler *mockapi.Handler
17+
projectID string
18+
mainEnv *mockapi.Environment
19+
factory *cmdFactory
20+
}
21+
22+
// setupVariableTest creates the common test infrastructure for variable tests.
23+
func setupVariableTest(t *testing.T) *variableTestSetup {
1324
authServer := mockapi.NewAuthServer(t)
14-
defer authServer.Close()
25+
t.Cleanup(authServer.Close)
1526

1627
apiHandler := mockapi.NewHandler(t)
1728
apiServer := httptest.NewServer(apiHandler)
18-
defer apiServer.Close()
29+
t.Cleanup(apiServer.Close)
1930

2031
projectID := mockapi.ProjectID()
2132

2233
apiHandler.SetProjects([]*mockapi.Project{{
2334
ID: projectID,
24-
Links: mockapi.MakeHALLinks("self=/projects/"+projectID,
25-
"environments=/projects/"+projectID+"/environments"),
35+
Links: mockapi.MakeHALLinks(
36+
"self=/projects/"+projectID,
37+
"environments=/projects/"+projectID+"/environments",
38+
),
2639
DefaultBranch: "main",
2740
}})
28-
main := makeEnv(projectID, "main", "production", "active", nil)
29-
main.Links["#variables"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/variables"}
30-
main.Links["#manage-variables"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/variables"}
31-
envs := []*mockapi.Environment{main}
32-
apiHandler.SetEnvironments(envs)
3341

34-
apiHandler.SetProjectVariables(projectID, []*mockapi.Variable{
42+
mainEnv := makeEnv(projectID, "main", "production", "active", nil)
43+
mainEnv.Links["#variables"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/variables"}
44+
mainEnv.Links["#manage-variables"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/variables"}
45+
46+
return &variableTestSetup{
47+
authServer: authServer,
48+
apiServer: apiServer,
49+
apiHandler: apiHandler,
50+
projectID: projectID,
51+
mainEnv: mainEnv,
52+
factory: newCommandFactory(t, apiServer.URL, authServer.URL),
53+
}
54+
}
55+
56+
func TestVariableCreate(t *testing.T) {
57+
s := setupVariableTest(t)
58+
s.apiHandler.SetEnvironments([]*mockapi.Environment{s.mainEnv})
59+
s.apiHandler.SetProjectVariables(s.projectID, []*mockapi.Variable{
3560
{
3661
Name: "existing",
3762
IsSensitive: true,
3863
VisibleBuild: true,
3964
},
4065
})
4166

42-
f := newCommandFactory(t, apiServer.URL, authServer.URL)
67+
f, p := s.factory, s.projectID
4368

4469
//nolint:lll
45-
_, stdErr, err := f.RunCombinedOutput("var:create", "-p", projectID, "-l", "e", "-e", "main", "env:TEST", "--value", "env-level-value")
70+
_, stdErr, err := f.RunCombinedOutput("var:create", "-p", p, "-l", "e", "-e", "main", "env:TEST", "--value", "env-level-value")
4671
assert.NoError(t, err)
4772
assert.Contains(t, stdErr, "Creating variable env:TEST on the environment main")
4873

49-
assertTrimmed(t, "env-level-value", f.Run("var:get", "-p", projectID, "-e", "main", "env:TEST", "-P", "value"))
74+
assertTrimmed(t, "env-level-value", f.Run("var:get", "-p", p, "-e", "main", "env:TEST", "-P", "value"))
5075

51-
//nolint:lll
52-
_, stdErr, err = f.RunCombinedOutput("var:create", "-p", projectID, "env:TEST", "-l", "p", "--value", "project-level-value")
76+
_, stdErr, err = f.RunCombinedOutput("var:create", "-p", p, "env:TEST", "-l", "p", "--value", "project-level-value")
5377
assert.NoError(t, err)
54-
assert.Contains(t, stdErr, "Creating variable env:TEST on the project "+projectID)
78+
assert.Contains(t, stdErr, "Creating variable env:TEST on the project "+p)
5579

56-
//nolint:lll
57-
assertTrimmed(t, "project-level-value", f.Run("var:get", "-p", projectID, "-e", "main", "env:TEST", "-P", "value", "-l", "p"))
58-
//nolint:lll
59-
assertTrimmed(t, "env-level-value", f.Run("var:get", "-p", projectID, "-e", "main", "env:TEST", "-P", "value", "-l", "e"))
80+
assertTrimmed(t, "project-level-value", f.Run("var:get", "-p", p, "-e", "main", "env:TEST", "-P", "value", "-l", "p"))
81+
assertTrimmed(t, "env-level-value", f.Run("var:get", "-p", p, "-e", "main", "env:TEST", "-P", "value", "-l", "e"))
6082

61-
_, stdErr, err = f.RunCombinedOutput("var:create", "-p", projectID, "existing", "-l", "p", "--value", "test")
83+
_, stdErr, err = f.RunCombinedOutput("var:create", "-p", p, "existing", "-l", "p", "--value", "test")
6284
assert.Error(t, err)
6385
assert.Contains(t, stdErr, "The variable already exists")
6486

65-
//nolint:lll
66-
_, _, err = f.RunCombinedOutput("var:update", "-p", projectID, "env:TEST", "-l", "p", "--value", "project-level-value2")
87+
_, _, err = f.RunCombinedOutput("var:update", "-p", p, "env:TEST", "-l", "p", "--value", "project-level-value2")
6788
assert.NoError(t, err)
68-
assertTrimmed(t, "project-level-value2", f.Run("var:get", "-p", projectID, "env:TEST", "-l", "p", "-P", "value"))
89+
assertTrimmed(t, "project-level-value2", f.Run("var:get", "-p", p, "env:TEST", "-l", "p", "-P", "value"))
6990

70-
assertTrimmed(t, "true", f.Run("var:get", "-p", projectID, "env:TEST", "-l", "p", "-P", "visible_runtime"))
71-
_, _, err = f.RunCombinedOutput("var:update", "-p", projectID, "env:TEST", "-l", "p", "--visible-runtime", "false")
91+
assertTrimmed(t, "true", f.Run("var:get", "-p", p, "env:TEST", "-l", "p", "-P", "visible_runtime"))
92+
_, _, err = f.RunCombinedOutput("var:update", "-p", p, "env:TEST", "-l", "p", "--visible-runtime", "false")
7293
assert.NoError(t, err)
73-
assertTrimmed(t, "false", f.Run("var:get", "-p", projectID, "env:TEST", "-l", "p", "-P", "visible_runtime"))
94+
assertTrimmed(t, "false", f.Run("var:get", "-p", p, "env:TEST", "-l", "p", "-P", "visible_runtime"))
95+
}
96+
97+
func TestVariableCreateWithAppScope(t *testing.T) {
98+
s := setupVariableTest(t)
99+
100+
// Set up deployment with app names for validation.
101+
s.mainEnv.SetCurrentDeployment(&mockapi.Deployment{
102+
WebApps: map[string]mockapi.App{
103+
"app1": {Name: "app1", Type: "golang:1.23"},
104+
"app2": {Name: "app2", Type: "php:8.3"},
105+
},
106+
Routes: make(map[string]any),
107+
Links: mockapi.MakeHALLinks("self=/projects/" + s.projectID + "/environments/main/deployment/current"),
108+
})
109+
s.apiHandler.SetEnvironments([]*mockapi.Environment{s.mainEnv})
110+
111+
f, p := s.factory, s.projectID
112+
113+
// Test creating project-level variable with single app-scope.
114+
_, stdErr, err := f.RunCombinedOutput("var:create", "-p", p, "-l", "p",
115+
"env:SCOPED", "--value", "val", "--app-scope", "app1")
116+
assert.NoError(t, err)
117+
assert.Contains(t, stdErr, "Creating variable env:SCOPED")
118+
119+
// Verify application_scope was set.
120+
out := f.Run("var:get", "-p", p, "-l", "p", "env:SCOPED", "-P", "application_scope")
121+
assert.Contains(t, out, "app1")
122+
123+
// Test creating variable with multiple app scopes.
124+
_, _, err = f.RunCombinedOutput("var:create", "-p", p, "-l", "p",
125+
"env:MULTI", "--value", "val", "--app-scope", "app1", "--app-scope", "app2")
126+
assert.NoError(t, err)
127+
128+
out = f.Run("var:get", "-p", p, "-l", "p", "env:MULTI", "-P", "application_scope")
129+
assert.Contains(t, out, "app1")
130+
assert.Contains(t, out, "app2")
131+
132+
// Test validation rejects invalid app names (when deployment exists).
133+
_, stdErr, err = f.RunCombinedOutput("var:create", "-p", p, "-l", "p",
134+
"env:BAD", "--value", "val", "--app-scope", "nonexistent")
135+
assert.Error(t, err)
136+
assert.Contains(t, stdErr, "was not found")
137+
138+
// Test updating app-scope.
139+
_, _, err = f.RunCombinedOutput("var:update", "-p", p, "-l", "p",
140+
"env:SCOPED", "--app-scope", "app2")
141+
assert.NoError(t, err)
142+
143+
out = f.Run("var:get", "-p", p, "-l", "p", "env:SCOPED", "-P", "application_scope")
144+
assert.Contains(t, out, "app2")
145+
}
146+
147+
func TestVariableCreateWithAppScopeNoDeployment(t *testing.T) {
148+
// Uses an environment without a deployment, so app-scope validation is skipped.
149+
s := setupVariableTest(t)
150+
s.apiHandler.SetEnvironments([]*mockapi.Environment{s.mainEnv})
151+
152+
f, p := s.factory, s.projectID
153+
154+
// Without a deployment, any app-scope value should be accepted.
155+
_, stdErr, err := f.RunCombinedOutput("var:create", "-p", p, "-l", "p",
156+
"env:ANY_APP", "--value", "val", "--app-scope", "anyapp")
157+
assert.NoError(t, err)
158+
assert.Contains(t, stdErr, "Creating variable env:ANY_APP")
159+
160+
out := f.Run("var:get", "-p", p, "-l", "p", "env:ANY_APP", "-P", "application_scope")
161+
assert.Contains(t, out, "anyapp")
74162
}

legacy/CONTRIBUTING.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
# Contributing
22

3-
Development of the Platform.sh Legacy CLI happens in public in the
4-
[GitHub repository](https://github.com/platformsh/legacy-cli).
3+
Development of the Legacy CLI happens in public in the [GitHub repository](https://github.com/platformsh/legacy-cli).
54

65
Issues and pull requests submitted via GitHub are very welcome.
76

8-
In the near future - to be confirmed - this may move to being a subtree split
9-
of the new CLI repository at: https://github.com/platformsh/cli
7+
The principal Upsun CLI repository is: https://github.com/platformsh/cli
108

119
## Developing locally
1210

@@ -30,8 +28,8 @@ Run tests with:
3028

3129
## Developing in a docker container
3230

33-
If you don't have PHP installed locally or for other reasons want to do development on the
34-
Platform.sh CLI inside a docker container, follow this procedure:
31+
If you don't have PHP installed locally or for other reasons want to do development on the CLI inside a docker
32+
container, follow this procedure:
3533

3634
Create a `.env` file based on the default one
3735

legacy/Makefile

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
GO_TESTS_DIR=go-tests
2-
31
.PHONY: composer-dev
42
composer-dev:
53
composer install --no-interaction
@@ -16,19 +14,9 @@ lint-phpstan: composer-dev
1614
lint-php-cs-fixer: composer-dev
1715
./vendor/bin/php-cs-fixer check --config .php-cs-fixer.dist.php --diff
1816

19-
.PHONY: lint-gofmt
20-
lint-gofmt:
21-
cd $(GO_TESTS_DIR) && go fmt ./...
22-
23-
.PHONY: lint-golangci
24-
lint-golangci:
25-
command -v golangci-lint >/dev/null || go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
26-
cd $(GO_TESTS_DIR) && golangci-lint run
27-
2817
.PHONY: lint
29-
lint: lint-gofmt lint-golangci lint-php-cs-fixer lint-phpstan
18+
lint: lint-php-cs-fixer lint-phpstan
3019

3120
.PHONY: test
3221
test:
3322
./vendor/bin/phpunit --exclude-group slow
34-
cd $(GO_TESTS_DIR) && go test -v -count=1 ./...

legacy/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
The **Legacy** Platform.sh CLI is the legacy version of the command-line interface for [Platform.sh](https://platform.sh). For the **current Platform.sh CLI**, check [this repository](https://github.com/platformsh/cli).
1+
The **Legacy** CLI is the legacy version of the command-line interface for [Upsun (formerly Platform.sh)](https://upsun.com).
2+
3+
For the **current Upsun CLI**, check [this repository](https://github.com/platformsh/cli).
24

35
## Install
46

@@ -53,7 +55,7 @@ scoop update platform
5355

5456
## Usage
5557

56-
You can run the Platform.sh CLI in your shell by typing `platform`.
58+
You can run this CLI in your shell by typing `platform`.
5759

5860
platform
5961

@@ -96,7 +98,7 @@ Other customization is available via environment variables, including:
9698
* `PLATFORMSH_CLI_SSH_AUTO_LOAD_CERT`: set to 0 to disable automatic loading of an SSH certificate when running login or SSH commands
9799
* `PLATFORMSH_CLI_REPORT_DEPRECATIONS`: set to 1 to enable PHP deprecation notices (suppressed by default). They will only be displayed in debug mode (`-vvv`).
98100
* `CLICOLOR_FORCE`: set to 1 or 0 to force colorized output on or off, respectively
99-
* `http_proxy` or `https_proxy`: specify a proxy for connecting to Platform.sh
101+
* `http_proxy` or `https_proxy`: specify an HTTP proxy
100102

101103
## Known issues
102104

legacy/config-defaults.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ detection:
363363
- Unable to find stack
364364
- Cannot build application of type
365365
- Invalid deployment
366+
- Could not perform a rolling deployment
366367

367368
# Pagination settings.
368369
#
@@ -425,9 +426,8 @@ browser_login:
425426
# css: ''
426427
body: |
427428
<img
428-
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAQMAAABKLAcXAAAABlBMVEUAAADg4ODy8Xj7AAAAAXRSTlMAQObYZgAAAB5JREFUOMtj+I8EPozyRnlU4w1NMJhCcDT+hm2MAQAJBMb6YxK/8wAAAABJRU5ErkJggg=="
429+
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJ1dWlkLTk2NDZkYjJkLTc3NjItNDc3Yy05MWMzLWE3OGZhNmY3ZTYzMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDAzLjM3IDI0OS4zIj48cGF0aCBkPSJtMTYzLjg4LDE0My40NmMtMi42LDAtNC44Mi0uNTctNi42Ny0xLjctMS44NS0xLjEzLTMuMjYtMi42NC00LjIyLTQuNTQtLjk2LTEuOS0xLjQ0LTQuMDMtMS40NC02LjM4di0yNC44OWgxMC44MnYyMi44YzAsMi4wMi41LDMuNTQsMS41MSw0LjU0czIuNDMsMS41MSw0LjI2LDEuNTFjMS42MywwLDMuMDgtLjM4LDQuMzMtMS4xNSwxLjI1LS43NywyLjI1LTEuODQsMi45OS0zLjIxLjc1LTEuMzcsMS4xMi0yLjk1LDEuMTItNC43MmwuOTQsOC44N2MtMS4yLDIuNjUtMi45Niw0Ljc5LTUuMjcsNi40Mi0yLjMxLDEuNjQtNS4xLDIuNDUtOC4zNywyLjQ1Wm0xMi45MS0uNzJ2LTguNjZoLS4yMnYtMjguMTNoMTAuODJ2MzYuNzloLTEwLjZaIi8+PHBhdGggZD0ibTIxNi42OSwxNDMuNDZjLTMuNTEsMC02LjMyLS44Mi04LjQ0LTIuNDUtMi4xMi0xLjYzLTMuMzctMy44Ny0zLjc1LTYuNzFsLjU4LS4wN3YyMy4zaC0xMC44MnYtNTEuNThoMTAuNnY4LjE1bC0uNjUtLjE0Yy41My0yLjY5LDEuOTctNC44Miw0LjMzLTYuMzgsMi4zNi0xLjU2LDUuMjctMi4zNCw4LjczLTIuMzRzNi4xOS43OCw4LjYyLDIuMzRjMi40MywxLjU2LDQuMywzLjc2LDUuNjMsNi42LDEuMzIsMi44NCwxLjk4LDYuMTgsMS45OCwxMC4wM3MtLjcsNy4yOS0yLjA5LDEwLjE3Yy0xLjQsMi44OS0zLjM0LDUuMTItNS44NCw2LjcxLTIuNSwxLjU5LTUuNDYsMi4zOC04Ljg3LDIuMzhabS0zLjAzLTguNjZjMi41NSwwLDQuNjItLjkxLDYuMi0yLjc0LDEuNTktMS44MywyLjM4LTQuNDIsMi4zOC03Ljc5cy0uODEtNS45NC0yLjQyLTcuNzJjLTEuNjEtMS43OC0zLjcyLTIuNjctNi4zMS0yLjY3cy00LjU2LjktNi4xNywyLjcxYy0xLjYxLDEuOC0yLjQyLDQuMzktMi40Miw3Ljc1cy44LDUuOTUsMi40Miw3Ljc1YzEuNjEsMS44LDMuNzIsMi43MSw2LjMxLDIuNzFaIi8+PHBhdGggZD0ibTI1NC40MiwxNDMuNDZjLTUuMzksMC05LjY3LTEuMDgtMTIuODQtMy4yNS0zLjE3LTIuMTctNC45MS01LjE1LTUuMTktOC45NWg5LjY3Yy4yNCwxLjYzLDEuMDcsMi44NywyLjQ5LDMuNzEsMS40Mi44NCwzLjM4LDEuMjYsNS44OCwxLjI2LDIuMjYsMCwzLjkxLS4zMyw0Ljk0LS45NywxLjAzLS42NSwxLjU1LTEuNTcsMS41NS0yLjc4LDAtLjkxLS4zLTEuNjItLjktMi4xMy0uNi0uNS0xLjctLjkyLTMuMjgtMS4yNmwtNS45Mi0xLjIzYy00LjM4LS45MS03LjYtMi4zLTkuNjctNC4xNS0yLjA3LTEuODUtMy4xLTQuMjQtMy4xLTcuMTgsMC0zLjU2LDEuMzctNi4zNCw0LjExLTguMzMsMi43NC0yLDYuNTYtMi45OSwxMS40Ny0yLjk5czguNzMuOTcsMTEuNjEsMi45MmMyLjg5LDEuOTUsNC40Nyw0LjY1LDQuNzYsOC4xMmgtOS42N2MtLjE5LTEuMjUtLjg3LTIuMi0yLjAyLTIuODVzLTIuNzktLjk3LTQuOTEtLjk3Yy0xLjkyLDAtMy4zNS4yOC00LjI5LjgzLS45NC41NS0xLjQxLDEuMzMtMS40MSwyLjM0LDAsLjg3LjM4LDEuNTUsMS4xNSwyLjA2Ljc3LjUsMi4wNC45NSwzLjgyLDEuMzNsNi42NCwxLjM3YzMuNy43Nyw2LjUsMi4yLDguNCw0LjI5LDEuOSwyLjA5LDIuODUsNC41NiwyLjg1LDcuMzksMCwzLjYxLTEuNDEsNi40MS00LjIyLDguNC0yLjgxLDItNi43OSwyLjk5LTExLjk0LDIuOTlaIi8+PHBhdGggZD0ibTI4Ni45NiwxNDMuNDZjLTIuNiwwLTQuODItLjU3LTYuNjctMS43LTEuODUtMS4xMy0zLjI2LTIuNjQtNC4yMi00LjU0LS45Ni0xLjktMS40NC00LjAzLTEuNDQtNi4zOHYtMjQuODloMTAuODJ2MjIuOGMwLDIuMDIuNSwzLjU0LDEuNTEsNC41NCwxLjAxLDEuMDEsMi40MywxLjUxLDQuMjYsMS41MSwxLjYzLDAsMy4wOC0uMzgsNC4zMy0xLjE1LDEuMjUtLjc3LDIuMjUtMS44NCwyLjk5LTMuMjEuNzQtMS4zNywxLjEyLTIuOTUsMS4xMi00LjcybC45NCw4Ljg3Yy0xLjIsMi42NS0yLjk2LDQuNzktNS4yNyw2LjQyLTIuMzEsMS42NC01LjEsMi40NS04LjM3LDIuNDVabTEyLjkxLS43MnYtOC42NmgtLjIydi0yOC4xM2gxMC44MnYzNi43OWgtMTAuNloiLz48cGF0aCBkPSJtMzE3LjMzLDE0Mi43M3YtMzYuNzloMTAuNnY4LjY2aC4yMnYyOC4xM2gtMTAuODJabTI1LjYxLDB2LTIyLjhjMC0yLjAyLS41Mi0zLjU0LTEuNTUtNC41NC0xLjAzLTEuMDEtMi41NC0xLjUxLTQuNTEtMS41MS0xLjY4LDAtMy4xOS4zOS00LjUxLDEuMTUtMS4zMi43Ny0yLjM2LDEuODMtMy4xLDMuMTctLjc1LDEuMzUtMS4xMiwyLjkzLTEuMTIsNC43NmwtLjk0LTguODdjMS4yLTIuNjksMi45Ny00Ljg0LDUuMy02LjQ2LDIuMzMtMS42MSw1LjIxLTIuNDIsOC42Mi0yLjQyLDQuMDksMCw3LjIxLDEuMTQsOS4zOCwzLjQzLDIuMTYsMi4yOSwzLjI1LDUuMzUsMy4yNSw5LjJ2MjQuODloLTEwLjgyWiIvPjxnIGlkPSJ1dWlkLTg2Yjg5MTUxLWU2NWMtNGRhZi1iNWM2LTI2ZmEwYzhlMzk0ZCI+PHBhdGggZD0ibTg5Ljc5LDEwMy4wNGMxMS45NiwwLDIxLjYzLDkuNjksMjEuNjMsMjEuNjNoMjEuNjNjMC0yMy44OS0xOS4zNi00My4yNy00My4yNy00My4yN3MtNDMuMjcsMTkuMzYtNDMuMjcsNDMuMjdoMjEuNjNjLjA1LTExLjk2LDkuNzQtMjEuNjMsMjEuNjctMjEuNjNaIi8+PHBhdGggZD0ibTk3LjEyLDE0NWM4LjM0LTMuMDEsMTQuMjktMTAuOTgsMTQuMjktMjAuMzRoLTQzLjI1YzAsOS4zNiw1Ljk2LDE3LjM1LDE0LjI5LDIwLjM0di4zN2gtMzAuNjRjNy4zMywxMy40MywyMS42LDIyLjU0LDM4LDIyLjU0czMwLjY0LTkuMTEsMzgtMjIuNTRoLTMwLjY5di0uMzdaIi8+PC9nPjwvc3ZnPg=="
429430
alt=""
430-
width="100"
431431
height="100"
432432
class="icon">
433433

0 commit comments

Comments
 (0)