Skip to content

Commit 5b8e7b2

Browse files
committed
Merge branch 'charmed-snapcraft-io' into staging
2 parents 94a82ef + 0140e5a commit 5b8e7b2

8 files changed

Lines changed: 556 additions & 63 deletions

File tree

.github/workflows/deploy.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ on:
1717
jobs:
1818
setup:
1919
runs-on: ubuntu-latest
20+
permissions:
21+
contents: read
2022
environment: ${{ github.event.inputs.environment != '' && github.event.inputs.environment || (github.ref == 'refs/heads/main' && 'Production' || 'Staging') }}
2123
outputs:
2224
charm_name: ${{ steps.setup-vars.outputs.charm_name }}
@@ -38,6 +40,9 @@ jobs:
3840
needs: setup
3941
name: Deploy
4042
uses: canonical/webteam-devops/.github/workflows/deploy.yaml@main
43+
permissions:
44+
contents: read
45+
deployments: write
4146
with:
4247
environment: ${{ needs.setup.outputs.environment }}
4348
charm_name: ${{ needs.setup.outputs.charm_name }}

.github/workflows/pr.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ jobs:
9090
9191
pack-rock:
9292
runs-on: ubuntu-latest
93+
permissions:
94+
contents: read
9395
steps:
9496
- uses: actions/checkout@v4
9597

HACKING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ To use staging APIs locally you can add the following lines to an `.env.local` f
77

88
```bash
99
DEVICE_GATEWAY_API_URL=https://api.staging.snapcraft.io/
10-
DASHBOARD_API_URL=https://dashboard.staging.snapcraft.io/
10+
SNAPSTORE_DASHBOARD_API_URL=https://dashboard.staging.snapcraft.io/
1111
LOGIN_URL=https://login.staging.ubuntu.com
1212
CANDID_API_URL=https://api.staging.jujucharms.com/identity/
1313
```

konf/staging-api.snapcraft.io.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ env:
66
- name: SNAPSTORE_DASHBOARD_API_URL
77
value: https://dashboard.staging.snapcraft.io/
88

9-
- name: DASHBOARD_API_URL
10-
value: https://dashboard.staging.snapcraft.io/
11-
129
- name: LOGIN_URL
1310
value: https://login.staging.ubuntu.com
1411

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ canonicalwebteam.discourse==7.0.0
55
canonicalwebteam.blog==6.5.0
66
canonicalwebteam.search==2.1.2
77
canonicalwebteam.image-template==1.9.0
8-
canonicalwebteam.store-api==7.3.1
8+
canonicalwebteam.store-api==7.3.3
99
canonicalwebteam.launchpad==0.9.0
1010
django-openid-auth==0.17
1111
Flask-OpenID==1.3.0

static/js/publisher/pages/Builds/RepoSelector.tsx

Lines changed: 136 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { ChangeEvent, Dispatch, SetStateAction, useState } from "react";
1+
import {
2+
ChangeEvent,
3+
Dispatch,
4+
SetStateAction,
5+
useState,
6+
useEffect,
7+
useRef,
8+
} from "react";
29
import { useParams } from "react-router-dom";
310
import { useSetAtom } from "jotai";
411
import {
@@ -21,6 +28,14 @@ type Props = {
2128
setAutoTriggerBuild: Dispatch<SetStateAction<boolean>>;
2229
};
2330

31+
// Utility function for formatting repository names with owner
32+
const getRepoNameWithOwner = (
33+
selectedOrg: string | null,
34+
repo: Repo,
35+
): string => {
36+
return selectedOrg ? `${selectedOrg}/${repo.name}` : repo.nameWithOwner;
37+
};
38+
2439
function generateYamlTemplateUrl(
2540
org: string | null,
2641
repo: string | undefined,
@@ -67,55 +82,79 @@ function RepoSelector({ githubData, setAutoTriggerBuild }: Props) {
6782
const [reposLoading, setReposLoading] = useState<boolean>(false);
6883
const [validRepo, setValidRepo] = useState<boolean | null>(null);
6984
const [validationError, setValidationError] = useState<boolean>(false);
70-
const [missingYaml, setMissingYaml] = useState<boolean>(false);
71-
const [defaultBranch, setDefaultBranch] = useState<string | null>(null);
85+
const [repoInputValue, setRepoInputValue] = useState<string>("");
86+
const [repoFetchError, setRepoFetchError] = useState<string>("");
87+
const [repoConnectError, setRepoConnectError] = useState<string>("");
7288

73-
const getRepoNameWithOwner = (repo: Repo) =>
74-
selectedOrg ? `${selectedOrg}/${repo.name}` : repo.nameWithOwner;
89+
const [defaultBranch, setDefaultBranch] = useState<string | null>(null);
7590

76-
const validateRepo = async (repo: Repo | undefined) => {
77-
setMissingYaml(false);
91+
// Track autofill attempts to avoid unnecessary re-runs
92+
const autofillAttemptedRef = useRef<string | null>(null);
7893

94+
const validateRepoInternal = async (repo: Repo | undefined) => {
7995
if (!repo) {
8096
return;
8197
}
8298

8399
setValidating(true);
84100

85-
const repoName = getRepoNameWithOwner(repo);
101+
const repoName = getRepoNameWithOwner(selectedOrg, repo);
86102

87-
const response = await fetch(
88-
`/api/${snapId}/builds/validate-repo?repo=${repoName}`,
89-
);
103+
try {
104+
const response = await fetch(
105+
`/api/${snapId}/builds/validate-repo?repo=${repoName}`,
106+
);
90107

91-
if (!response.ok) {
92-
setValidationError(true);
93-
throw new Error("Not a valid repo");
94-
}
108+
if (!response.ok) {
109+
setValidationError(true);
110+
throw new Error("Not a valid repo");
111+
}
95112

96-
const responseData = await response.json();
113+
const responseData = await response.json();
97114

98-
if (responseData.success) {
99-
setValidRepo(true);
100-
setValidationMessage("");
101-
setValidationError(false);
102-
setMissingYaml(false);
103-
} else {
104-
const error = responseData.error;
115+
if (responseData.success) {
116+
setValidRepo(true);
117+
setValidationMessage("");
118+
setValidationError(false);
119+
} else {
120+
const error = responseData.error;
105121

106-
if (error.type === "MISSING_YAML_FILE") {
107-
setMissingYaml(true);
108-
setDefaultBranch(responseData.data.default_branch);
109-
}
122+
if (error.type === "MISSING_YAML_FILE") {
123+
setDefaultBranch(responseData.data.default_branch);
124+
}
110125

111-
setValidationMessage(responseData.error.message);
126+
setValidationMessage(responseData.error.message);
127+
setValidationError(true);
128+
setValidRepo(null);
129+
}
130+
} catch {
112131
setValidationError(true);
113-
setValidRepo(null);
132+
} finally {
133+
setValidating(false);
114134
}
115-
116-
setValidating(false);
117135
};
118136

137+
// Autofill effect with proper dependencies
138+
useEffect(() => {
139+
const currentOrgKey = `${selectedOrg || "user"}-${snapId}`;
140+
141+
if (
142+
repos.length > 0 &&
143+
snapId &&
144+
!repoInputValue &&
145+
autofillAttemptedRef.current !== currentOrgKey
146+
) {
147+
const matchingRepo = repos.find((repo: Repo) => repo.name === snapId);
148+
if (matchingRepo) {
149+
setRepoInputValue(matchingRepo.name);
150+
setSelectedRepo(matchingRepo);
151+
validateRepoInternal(matchingRepo);
152+
}
153+
// Mark autofill as attempted for this org/snap combination
154+
autofillAttemptedRef.current = currentOrgKey;
155+
}
156+
}, [repos, snapId, repoInputValue, selectedOrg]);
157+
119158
const getRepos = async (org?: string) => {
120159
setReposLoading(true);
121160

@@ -125,17 +164,21 @@ function RepoSelector({ githubData, setAutoTriggerBuild }: Props) {
125164
apiUrl += `?org=${org}`;
126165
}
127166

128-
const response = await fetch(apiUrl);
129-
130-
if (!response.ok) {
131-
throw Error("Unable to fetch repos");
132-
}
133-
134-
const responseData = await response.json();
167+
try {
168+
const response = await fetch(apiUrl);
135169

136-
setReposLoading(false);
170+
if (!response.ok) {
171+
throw Error("Unable to fetch repos");
172+
}
137173

138-
setRepos(responseData);
174+
const responseData = await response.json();
175+
setRepos(responseData);
176+
} catch (_error) {
177+
setRepoFetchError("Failed to fetch repositories. Please try again.");
178+
setRepos([]);
179+
} finally {
180+
setReposLoading(false);
181+
}
139182
};
140183

141184
const getOrgs = (): { label: string; value: string }[] => {
@@ -166,9 +209,16 @@ function RepoSelector({ githubData, setAutoTriggerBuild }: Props) {
166209
const handleOrganizationChange = (e: ChangeEvent) => {
167210
const target = e.target as HTMLInputElement;
168211

169-
if (target.value) {
170-
setReposLoading(true);
212+
setRepoInputValue("");
213+
setSelectedRepo(undefined);
214+
setValidRepo(null);
215+
setValidationError(false);
216+
setRepoFetchError("");
217+
setRepoConnectError("");
218+
setRepos([]);
219+
autofillAttemptedRef.current = null;
171220

221+
if (target.value) {
172222
const org = target.value;
173223

174224
if (org === githubData.github_user.login) {
@@ -187,39 +237,53 @@ function RepoSelector({ githubData, setAutoTriggerBuild }: Props) {
187237
const target = e.target as HTMLInputElement;
188238
const searchTerm = target.value;
189239

240+
setRepoInputValue(searchTerm);
241+
setRepoFetchError("");
242+
setRepoConnectError("");
243+
190244
if (!searchTerm) {
191245
setValidationError(false);
192-
setValidRepo(false);
246+
setValidRepo(null);
247+
setSelectedRepo(undefined);
248+
return;
193249
}
194250

195251
const selectedRepo = repos.find((repo: Repo) => repo.name === searchTerm);
196252

197253
setSelectedRepo(selectedRepo);
198254

199255
if (selectedRepo) {
200-
validateRepo(selectedRepo);
256+
validateRepoInternal(selectedRepo);
201257
}
202258
};
203259

204260
const connectRepo = async () => {
205261
setBuilding(true);
262+
setRepoConnectError("");
206263
const formData = new FormData();
207264
formData.set("csrf_token", window.CSRF_TOKEN);
208265
if (selectedRepo) {
209-
const repoName = getRepoNameWithOwner(selectedRepo);
266+
const repoName = getRepoNameWithOwner(selectedOrg, selectedRepo);
210267
formData.set("github_repository", repoName);
211268
}
212269

213-
const response = await fetch(`/api/${snapId}/builds`, {
214-
method: "POST",
215-
body: formData,
216-
});
270+
try {
271+
const response = await fetch(`/api/${snapId}/builds`, {
272+
method: "POST",
273+
body: formData,
274+
});
217275

218-
if (response) {
219-
setAutoTriggerBuild(true);
276+
if (response.ok) {
277+
setAutoTriggerBuild(true);
278+
setRepoConnected(true);
279+
} else {
280+
setRepoConnectError("Failed to connect repository. Please try again.");
281+
}
282+
} catch (_error) {
283+
setRepoConnectError("Failed to connect repository. Please try again.");
284+
} finally {
285+
setBuilding(false);
220286
}
221-
222-
setRepoConnected(true);
223287
};
224288

225289
return (
@@ -257,6 +321,7 @@ function RepoSelector({ githubData, setAutoTriggerBuild }: Props) {
257321
placeholder="Search your repos"
258322
disabled={selectedOrg === null || validating}
259323
className="p-form-validation__input"
324+
value={repoInputValue}
260325
onChange={handleRepoChange}
261326
/>
262327
{reposLoading ||
@@ -270,6 +335,11 @@ function RepoSelector({ githubData, setAutoTriggerBuild }: Props) {
270335
<option value={repo.name} key={repo.name} />
271336
))}
272337
</datalist>
338+
{repoFetchError && (
339+
<p className="p-form-validation__message" role="alert">
340+
{repoFetchError}
341+
</p>
342+
)}
273343
</Col>
274344
<Col size={2}>
275345
<div style={{ display: "flex", alignItems: "end", height: "100%" }}>
@@ -289,7 +359,7 @@ function RepoSelector({ githubData, setAutoTriggerBuild }: Props) {
289359
className="p-tooltip--btm-center"
290360
aria-describedby="recheck-tooltip"
291361
onClick={() => {
292-
validateRepo(selectedRepo);
362+
validateRepoInternal(selectedRepo);
293363
}}
294364
>
295365
<i className="p-icon--restart"></i>
@@ -298,14 +368,14 @@ function RepoSelector({ githubData, setAutoTriggerBuild }: Props) {
298368
role="tooltip"
299369
id="recheck-tooltip"
300370
>
301-
Re-check
371+
Check again
302372
</span>
303373
</Button>
304374
)}
305375
</div>
306376
</Col>
307377
</Row>
308-
{missingYaml && (
378+
{validationError && (
309379
<div className="u-fixed-width">
310380
<p>{validationMessage}</p>
311381
<p>
@@ -317,7 +387,7 @@ function RepoSelector({ githubData, setAutoTriggerBuild }: Props) {
317387
selectedOrg,
318388
selectedRepo?.name,
319389
defaultBranch,
320-
snapId,
390+
snapId || "",
321391
)}
322392
>
323393
get started with a template
@@ -333,6 +403,15 @@ function RepoSelector({ githubData, setAutoTriggerBuild }: Props) {
333403
</p>
334404
</div>
335405
)}
406+
{repoConnectError && (
407+
<Row>
408+
<Col size={12}>
409+
<p className="p-form-validation__message" role="alert">
410+
{repoConnectError}
411+
</p>
412+
</Col>
413+
</Row>
414+
)}
336415
</Strip>
337416
);
338417
}

0 commit comments

Comments
 (0)