Skip to content

Commit 3c98979

Browse files
committed
trustpub: Implement workflow path verification for GitLab
1 parent 890c9b4 commit 3c98979

File tree

6 files changed

+224
-0
lines changed

6 files changed

+224
-0
lines changed

app/controllers/crate/settings/new-trusted-publisher.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export default class NewTrustedPublisherController extends Controller {
3737
get verificationUrl() {
3838
if (this.publisher === 'GitHub' && this.namespace && this.project && this.workflow) {
3939
return `https://raw.githubusercontent.com/${this.namespace}/${this.project}/HEAD/.github/workflows/${this.workflow}`;
40+
} else if (this.publisher === 'GitLab' && this.namespace && this.project && this.workflow) {
41+
return `https://gitlab.com/${this.namespace}/${this.project}/-/raw/HEAD/${this.workflow}`;
4042
}
4143
}
4244

app/templates/crate/settings/new-trusted-publisher.gjs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
2525
class='publisher-select base-input'
2626
data-test-publisher
2727
{{on 'change' @controller.publisherChanged}}
28+
{{on 'change' (perform @controller.verifyWorkflowTask)}}
2829
>
2930
{{#each @controller.publishers as |publisher|}}
3031
<option value={{publisher}} selected={{eq @controller.publisher publisher}}>{{publisher}}</option>
@@ -224,6 +225,7 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
224225
data-test-namespace
225226
{{autoFocus}}
226227
{{on 'input' @controller.resetNamespaceValidation}}
228+
{{on 'input' (perform @controller.verifyWorkflowTask)}}
227229
/>
228230

229231
{{#if @controller.namespaceInvalid}}
@@ -252,6 +254,7 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
252254
class='input base-input'
253255
data-test-project
254256
{{on 'input' @controller.resetProjectValidation}}
257+
{{on 'input' (perform @controller.verifyWorkflowTask)}}
255258
/>
256259

257260
{{#if @controller.projectInvalid}}
@@ -280,6 +283,7 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
280283
class='input base-input'
281284
data-test-workflow
282285
{{on 'input' @controller.resetWorkflowValidation}}
286+
{{on 'input' (perform @controller.verifyWorkflowTask)}}
283287
/>
284288

285289
{{#if @controller.workflowInvalid}}
@@ -302,6 +306,41 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
302306
<code>ci/publish.yml</code>.
303307
</div>
304308
{{/if}}
309+
310+
{{#if (not @controller.verificationUrl)}}
311+
<div class='workflow-verification' data-test-workflow-verification='initial'>
312+
The workflow filepath will be verified once all necessary fields are filled.
313+
</div>
314+
{{else if (eq @controller.verifyWorkflowTask.last.value 'success')}}
315+
<div class='workflow-verification workflow-verification--success' data-test-workflow-verification='success'>
316+
✓ Workflow file found at
317+
<a href='{{@controller.verificationUrl}}' target='_blank' rel='noopener noreferrer'>
318+
{{@controller.verificationUrl}}
319+
</a>
320+
</div>
321+
{{else if (eq @controller.verifyWorkflowTask.last.value 'not-found')}}
322+
<div
323+
class='workflow-verification workflow-verification--warning'
324+
data-test-workflow-verification='not-found'
325+
>
326+
⚠ Workflow file not found at
327+
<a href='{{@controller.verificationUrl}}' target='_blank' rel='noopener noreferrer'>
328+
{{@controller.verificationUrl}}
329+
</a>
330+
</div>
331+
{{else if (eq @controller.verifyWorkflowTask.last.value 'error')}}
332+
<div class='workflow-verification workflow-verification--warning' data-test-workflow-verification='error'>
333+
⚠ Could not verify workflow file at
334+
<a href='{{@controller.verificationUrl}}' target='_blank' rel='noopener noreferrer'>
335+
{{@controller.verificationUrl}}
336+
</a>
337+
(network error)
338+
</div>
339+
{{else}}
340+
<div class='workflow-verification' data-test-workflow-verification='verifying'>
341+
Verifying...
342+
</div>
343+
{{/if}}
305344
{{/let}}
306345
</div>
307346

e2e/routes/crate/settings/new-trusted-publisher.spec.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,5 +502,93 @@ test.describe('Route | crate.settings.new-trusted-publisher', { tag: '@routes' }
502502
await expect(page).toHaveURL(`/crates/${crate.name}/settings`);
503503
await expect(page.locator('[data-test-gitlab-config]')).toHaveCount(0);
504504
});
505+
506+
test.describe('workflow verification', () => {
507+
test('success case (200 OK)', async ({ msw, page }) => {
508+
let { crate } = await prepare(msw);
509+
510+
await page.goto(`/crates/${crate.name}/settings/new-trusted-publisher`);
511+
await expect(page).toHaveURL(`/crates/${crate.name}/settings/new-trusted-publisher`);
512+
513+
// Select GitLab from the publisher dropdown
514+
await page.selectOption('[data-test-publisher]', 'GitLab');
515+
516+
await msw.worker.use(
517+
http.head('https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml', () => {
518+
return new HttpResponse(null, { status: 200 });
519+
}),
520+
);
521+
522+
await expect(page.locator('[data-test-workflow-verification="initial"]')).toHaveText(
523+
'The workflow filepath will be verified once all necessary fields are filled.',
524+
);
525+
526+
await page.fill('[data-test-namespace]', 'rust-lang');
527+
await page.fill('[data-test-project]', 'crates.io');
528+
await page.fill('[data-test-workflow]', '.gitlab-ci.yml');
529+
530+
await expect(page.locator('[data-test-workflow-verification="success"]')).toBeVisible();
531+
532+
await expect(page.locator('[data-test-workflow-verification="success"]')).toHaveText(
533+
'✓ Workflow file found at https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml',
534+
);
535+
});
536+
537+
test('not found case (404)', async ({ msw, page }) => {
538+
let { crate } = await prepare(msw);
539+
540+
await page.goto(`/crates/${crate.name}/settings/new-trusted-publisher`);
541+
await expect(page).toHaveURL(`/crates/${crate.name}/settings/new-trusted-publisher`);
542+
543+
// Select GitLab from the publisher dropdown
544+
await page.selectOption('[data-test-publisher]', 'GitLab');
545+
546+
await msw.worker.use(
547+
http.head('https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/missing.yml', () => {
548+
return new HttpResponse(null, { status: 404 });
549+
}),
550+
);
551+
552+
await page.fill('[data-test-namespace]', 'rust-lang');
553+
await page.fill('[data-test-project]', 'crates.io');
554+
await page.fill('[data-test-workflow]', 'missing.yml');
555+
556+
await expect(page.locator('[data-test-workflow-verification="not-found"]')).toBeVisible();
557+
558+
await expect(page.locator('[data-test-workflow-verification="not-found"]')).toHaveText(
559+
'⚠ Workflow file not found at https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/missing.yml',
560+
);
561+
562+
// Verify form can still be submitted
563+
await page.click('[data-test-add]');
564+
await expect(page).toHaveURL(`/crates/${crate.name}/settings`);
565+
});
566+
567+
test('server error (5xx)', async ({ msw, page }) => {
568+
let { crate } = await prepare(msw);
569+
570+
await page.goto(`/crates/${crate.name}/settings/new-trusted-publisher`);
571+
await expect(page).toHaveURL(`/crates/${crate.name}/settings/new-trusted-publisher`);
572+
573+
// Select GitLab from the publisher dropdown
574+
await page.selectOption('[data-test-publisher]', 'GitLab');
575+
576+
await msw.worker.use(
577+
http.head('https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml', () => {
578+
return new HttpResponse(null, { status: 500 });
579+
}),
580+
);
581+
582+
await page.fill('[data-test-namespace]', 'rust-lang');
583+
await page.fill('[data-test-project]', 'crates.io');
584+
await page.fill('[data-test-workflow]', '.gitlab-ci.yml');
585+
586+
await expect(page.locator('[data-test-workflow-verification="error"]')).toBeVisible();
587+
588+
await expect(page.locator('[data-test-workflow-verification="error"]')).toHaveText(
589+
'⚠ Could not verify workflow file at https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml (network error)',
590+
);
591+
});
592+
});
505593
});
506594
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { http, HttpResponse } from 'msw';
2+
3+
export default [
4+
http.head('https://gitlab.com/:owner/:project/-/raw/HEAD/:workflow_path', () => {
5+
return new HttpResponse(null, { status: 404 });
6+
}),
7+
];

packages/crates-io-msw/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import categoryHandlers from './handlers/categories.js';
33
import cratesHandlers from './handlers/crates.js';
44
import docsRsHandlers from './handlers/docs-rs.js';
55
import githubHandlers from './handlers/github.js';
6+
import gitlabHandlers from './handlers/gitlab.js';
67
import inviteHandlers from './handlers/invites.js';
78
import keywordHandlers from './handlers/keywords.js';
89
import metadataHandlers from './handlers/metadata.js';
@@ -35,6 +36,7 @@ export const handlers = [
3536
...cratesHandlers,
3637
...docsRsHandlers,
3738
...githubHandlers,
39+
...gitlabHandlers,
3840
...inviteHandlers,
3941
...keywordHandlers,
4042
...metadataHandlers,

tests/routes/crate/settings/new-trusted-publisher-test.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,5 +513,91 @@ module('Route | crate.settings.new-trusted-publisher', hooks => {
513513
assert.strictEqual(currentURL(), `/crates/${crate.name}/settings`);
514514
assert.dom('[data-test-gitlab-config]').exists({ count: 0 });
515515
});
516+
517+
module('workflow verification', function () {
518+
test('success case (200 OK)', async function (assert) {
519+
let { crate } = prepare(this);
520+
521+
await visit(`/crates/${crate.name}/settings/new-trusted-publisher`);
522+
assert.strictEqual(currentURL(), `/crates/${crate.name}/settings/new-trusted-publisher`);
523+
524+
// Select GitLab from the publisher dropdown
525+
await fillIn('[data-test-publisher]', 'GitLab');
526+
527+
this.worker.use(
528+
http.head('https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml', () => {
529+
return new HttpResponse(null, { status: 200 });
530+
}),
531+
);
532+
533+
assert
534+
.dom('[data-test-workflow-verification="initial"]')
535+
.hasText('The workflow filepath will be verified once all necessary fields are filled.');
536+
537+
await fillIn('[data-test-namespace]', 'rust-lang');
538+
await fillIn('[data-test-project]', 'crates.io');
539+
await fillIn('[data-test-workflow]', '.gitlab-ci.yml');
540+
541+
await waitFor('[data-test-workflow-verification="success"]');
542+
543+
let expected = '✓ Workflow file found at https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml';
544+
assert.dom('[data-test-workflow-verification="success"]').hasText(expected);
545+
});
546+
547+
test('not found case (404)', async function (assert) {
548+
let { crate } = prepare(this);
549+
550+
await visit(`/crates/${crate.name}/settings/new-trusted-publisher`);
551+
assert.strictEqual(currentURL(), `/crates/${crate.name}/settings/new-trusted-publisher`);
552+
553+
// Select GitLab from the publisher dropdown
554+
await fillIn('[data-test-publisher]', 'GitLab');
555+
556+
this.worker.use(
557+
http.head('https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/missing.yml', () => {
558+
return new HttpResponse(null, { status: 404 });
559+
}),
560+
);
561+
562+
await fillIn('[data-test-namespace]', 'rust-lang');
563+
await fillIn('[data-test-project]', 'crates.io');
564+
await fillIn('[data-test-workflow]', 'missing.yml');
565+
566+
await waitFor('[data-test-workflow-verification="not-found"]');
567+
568+
let expected = '⚠ Workflow file not found at https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/missing.yml';
569+
assert.dom('[data-test-workflow-verification="not-found"]').hasText(expected);
570+
571+
// Verify form can still be submitted
572+
await click('[data-test-add]');
573+
assert.strictEqual(currentURL(), `/crates/${crate.name}/settings`);
574+
});
575+
576+
test('server error (5xx)', async function (assert) {
577+
let { crate } = prepare(this);
578+
579+
await visit(`/crates/${crate.name}/settings/new-trusted-publisher`);
580+
assert.strictEqual(currentURL(), `/crates/${crate.name}/settings/new-trusted-publisher`);
581+
582+
// Select GitLab from the publisher dropdown
583+
await fillIn('[data-test-publisher]', 'GitLab');
584+
585+
this.worker.use(
586+
http.head('https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml', () => {
587+
return new HttpResponse(null, { status: 500 });
588+
}),
589+
);
590+
591+
await fillIn('[data-test-namespace]', 'rust-lang');
592+
await fillIn('[data-test-project]', 'crates.io');
593+
await fillIn('[data-test-workflow]', '.gitlab-ci.yml');
594+
595+
await waitFor('[data-test-workflow-verification="error"]');
596+
597+
let expected =
598+
'⚠ Could not verify workflow file at https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml (network error)';
599+
assert.dom('[data-test-workflow-verification="error"]').hasText(expected);
600+
});
601+
});
516602
});
517603
});

0 commit comments

Comments
 (0)