Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion app/controllers/crate/settings/new-trusted-publisher.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import Controller from '@ember/controller';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import Ember from 'ember';

import { task } from 'ember-concurrency';
import { rawTimeout, restartableTask, task } from 'ember-concurrency';

export default class NewTrustedPublisherController extends Controller {
@service notifications;
Expand Down Expand Up @@ -33,6 +34,34 @@ export default class NewTrustedPublisherController extends Controller {
}
}

get verificationUrl() {
if (this.repositoryOwner && this.repositoryName && this.workflowFilename) {
return `https://raw.githubusercontent.com/${this.repositoryOwner}/${this.repositoryName}/HEAD/.github/workflows/${this.workflowFilename}`;
}
}

verifyWorkflowTask = restartableTask(async () => {
let timeout = Ember.testing ? 0 : 500;
await rawTimeout(timeout);

let { verificationUrl } = this;
if (!verificationUrl) return null;

try {
let response = await fetch(verificationUrl, { method: 'HEAD' });

if (response.ok) {
return 'success';
} else if (response.status === 404) {
return 'not-found';
} else {
return 'error';
}
} catch {
return 'error';
}
});

saveConfigTask = task(async () => {
if (!this.validate()) return;

Expand Down
17 changes: 17 additions & 0 deletions app/templates/crate/settings/new-trusted-publisher.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,20 @@
.cancel-button {
border-radius: 4px;
}

.workflow-verification {
margin-top: var(--space-2xs);
font-size: 0.85em;

:global(a), :global(a):hover {
color: inherit;
}
}

.workflow-verification--success {
color: var(--green800);
}

.workflow-verification--warning {
color: var(--yellow700);
}
39 changes: 39 additions & 0 deletions app/templates/crate/settings/new-trusted-publisher.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import autoFocus from '@zestia/ember-auto-focus/modifiers/auto-focus';
import perform from 'ember-concurrency/helpers/perform';
import preventDefault from 'ember-event-helpers/helpers/prevent-default';
import eq from 'ember-truth-helpers/helpers/eq';
import not from 'ember-truth-helpers/helpers/not';

import LoadingSpinner from 'crates-io/components/loading-spinner';

Expand Down Expand Up @@ -51,6 +52,7 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
data-test-repository-owner
{{autoFocus}}
{{on 'input' @controller.resetRepositoryOwnerValidation}}
{{on 'input' (perform @controller.verifyWorkflowTask)}}
/>

{{#if @controller.repositoryOwnerInvalid}}
Expand Down Expand Up @@ -79,6 +81,7 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
class='input base-input'
data-test-repository-name
{{on 'input' @controller.resetRepositoryNameValidation}}
{{on 'input' (perform @controller.verifyWorkflowTask)}}
/>

{{#if @controller.repositoryNameInvalid}}
Expand Down Expand Up @@ -107,6 +110,7 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
class='input base-input'
data-test-workflow-filename
{{on 'input' @controller.resetWorkflowFilenameValidation}}
{{on 'input' (perform @controller.verifyWorkflowTask)}}
/>

{{#if @controller.workflowFilenameInvalid}}
Expand Down Expand Up @@ -140,6 +144,41 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
<code>publish.yml</code>.
</div>
{{/if}}

{{#if (not @controller.verificationUrl)}}
<div class='workflow-verification' data-test-workflow-verification='initial'>
The workflow filename will be verified once all necessary fields are filled.
</div>
{{else if (eq @controller.verifyWorkflowTask.last.value 'success')}}
<div class='workflow-verification workflow-verification--success' data-test-workflow-verification='success'>
✓ Workflow file found at
<a href='{{@controller.verificationUrl}}' target='_blank' rel='noopener noreferrer'>
{{@controller.verificationUrl}}
</a>
</div>
{{else if (eq @controller.verifyWorkflowTask.last.value 'not-found')}}
<div
class='workflow-verification workflow-verification--warning'
data-test-workflow-verification='not-found'
>
⚠ Workflow file not found at
<a href='{{@controller.verificationUrl}}' target='_blank' rel='noopener noreferrer'>
{{@controller.verificationUrl}}
</a>
</div>
{{else if (eq @controller.verifyWorkflowTask.last.value 'error')}}
<div class='workflow-verification workflow-verification--warning' data-test-workflow-verification='error'>
⚠ Could not verify workflow file at
<a href='{{@controller.verificationUrl}}' target='_blank' rel='noopener noreferrer'>
{{@controller.verificationUrl}}
</a>
(network error)
</div>
{{else}}
<div class='workflow-verification' data-test-workflow-verification='verifying'>
Verifying...
</div>
{{/if}}
{{/let}}
</div>

Expand Down
73 changes: 73 additions & 0 deletions e2e/routes/crate/settings/new-trusted-publisher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,77 @@ test.describe('Route | crate.settings.new-trusted-publisher', { tag: '@routes' }
});
}
});

test.describe('workflow verification', () => {
test('success case (200 OK)', async ({ msw, page }) => {
let { crate } = await prepare(msw);

await page.goto(`/crates/${crate.name}/settings/new-trusted-publisher`);
await expect(page).toHaveURL(`/crates/${crate.name}/settings/new-trusted-publisher`);

await msw.worker.use(
http.head('https://raw.githubusercontent.com/rust-lang/crates.io/HEAD/.github/workflows/ci.yml', () => {
return new HttpResponse(null, { status: 200 });
}),
);

await expect(page.locator('[data-test-workflow-verification="initial"]')).toHaveText(
'The workflow filename will be verified once all necessary fields are filled.',
);

await page.fill('[data-test-repository-owner]', 'rust-lang');
await page.fill('[data-test-repository-name]', 'crates.io');
await page.fill('[data-test-workflow-filename]', 'ci.yml');

await expect(page.locator('[data-test-workflow-verification="success"]')).toHaveText(
'✓ Workflow file found at https://raw.githubusercontent.com/rust-lang/crates.io/HEAD/.github/workflows/ci.yml',
);
});

test('not found case (404)', async ({ msw, page }) => {
let { crate } = await prepare(msw);

await page.goto(`/crates/${crate.name}/settings/new-trusted-publisher`);
await expect(page).toHaveURL(`/crates/${crate.name}/settings/new-trusted-publisher`);

await msw.worker.use(
http.head('https://raw.githubusercontent.com/rust-lang/crates.io/HEAD/.github/workflows/missing.yml', () => {
return new HttpResponse(null, { status: 404 });
}),
);

await page.fill('[data-test-repository-owner]', 'rust-lang');
await page.fill('[data-test-repository-name]', 'crates.io');
await page.fill('[data-test-workflow-filename]', 'missing.yml');

await expect(page.locator('[data-test-workflow-verification="not-found"]')).toHaveText(
'⚠ Workflow file not found at https://raw.githubusercontent.com/rust-lang/crates.io/HEAD/.github/workflows/missing.yml',
);

// Verify form can still be submitted
await page.click('[data-test-add]');
await expect(page).toHaveURL(`/crates/${crate.name}/settings`);
});

test('server error (5xx)', async ({ msw, page }) => {
let { crate } = await prepare(msw);

await page.goto(`/crates/${crate.name}/settings/new-trusted-publisher`);
await expect(page).toHaveURL(`/crates/${crate.name}/settings/new-trusted-publisher`);

await msw.worker.use(
http.head('https://raw.githubusercontent.com/rust-lang/crates.io/HEAD/.github/workflows/ci.yml', () => {
return new HttpResponse(null, { status: 500 });
}),
);

await page.fill('[data-test-repository-owner]', 'rust-lang');
await page.fill('[data-test-repository-name]', 'crates.io');
await page.fill('[data-test-workflow-filename]', 'ci.yml');

await expect(page.locator('[data-test-workflow-verification="error"]')).toHaveText(
'⚠ Could not verify workflow file at https://raw.githubusercontent.com/rust-lang/crates.io/HEAD/.github/workflows/ci.yml (network error)',
);
});
});
});
7 changes: 7 additions & 0 deletions packages/crates-io-msw/handlers/github.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { http, HttpResponse } from 'msw';

export default [
http.head('https://raw.githubusercontent.com/:owner/:projec/HEAD/.github/workflows/:workflow_filename', () => {
return new HttpResponse(null, { status: 404 });
}),
];
2 changes: 2 additions & 0 deletions packages/crates-io-msw/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import apiTokenHandlers from './handlers/api-tokens.js';
import categoryHandlers from './handlers/categories.js';
import cratesHandlers from './handlers/crates.js';
import docsRsHandlers from './handlers/docs-rs.js';
import githubHandlers from './handlers/github.js';
import inviteHandlers from './handlers/invites.js';
import keywordHandlers from './handlers/keywords.js';
import metadataHandlers from './handlers/metadata.js';
Expand Down Expand Up @@ -32,6 +33,7 @@ export const handlers = [
...categoryHandlers,
...cratesHandlers,
...docsRsHandlers,
...githubHandlers,
...inviteHandlers,
...keywordHandlers,
...metadataHandlers,
Expand Down
79 changes: 79 additions & 0 deletions tests/routes/crate/settings/new-trusted-publisher-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,83 @@ module('Route | crate.settings.new-trusted-publisher', hooks => {
});
}
});

module('workflow verification', function () {
test('success case (200 OK)', async function (assert) {
let { crate } = prepare(this);

await visit(`/crates/${crate.name}/settings/new-trusted-publisher`);
assert.strictEqual(currentURL(), `/crates/${crate.name}/settings/new-trusted-publisher`);

this.worker.use(
http.head('https://raw.githubusercontent.com/rust-lang/crates.io/HEAD/.github/workflows/ci.yml', () => {
return new HttpResponse(null, { status: 200 });
}),
);

assert
.dom('[data-test-workflow-verification="initial"]')
.hasText('The workflow filename will be verified once all necessary fields are filled.');

await fillIn('[data-test-repository-owner]', 'rust-lang');
await fillIn('[data-test-repository-name]', 'crates.io');
await fillIn('[data-test-workflow-filename]', 'ci.yml');

await waitFor('[data-test-workflow-verification="success"]');

let expected =
'✓ Workflow file found at https://raw.githubusercontent.com/rust-lang/crates.io/HEAD/.github/workflows/ci.yml';
assert.dom('[data-test-workflow-verification="success"]').hasText(expected);
});

test('not found case (404)', async function (assert) {
let { crate } = prepare(this);

await visit(`/crates/${crate.name}/settings/new-trusted-publisher`);
assert.strictEqual(currentURL(), `/crates/${crate.name}/settings/new-trusted-publisher`);

this.worker.use(
http.head('https://raw.githubusercontent.com/rust-lang/crates.io/HEAD/.github/workflows/missing.yml', () => {
return new HttpResponse(null, { status: 404 });
}),
);

await fillIn('[data-test-repository-owner]', 'rust-lang');
await fillIn('[data-test-repository-name]', 'crates.io');
await fillIn('[data-test-workflow-filename]', 'missing.yml');

await waitFor('[data-test-workflow-verification="not-found"]');

let expected =
'⚠ Workflow file not found at https://raw.githubusercontent.com/rust-lang/crates.io/HEAD/.github/workflows/missing.yml';
assert.dom('[data-test-workflow-verification="not-found"]').hasText(expected);

// Verify form can still be submitted
await click('[data-test-add]');
assert.strictEqual(currentURL(), `/crates/${crate.name}/settings`);
});

test('server error (5xx)', async function (assert) {
let { crate } = prepare(this);

await visit(`/crates/${crate.name}/settings/new-trusted-publisher`);
assert.strictEqual(currentURL(), `/crates/${crate.name}/settings/new-trusted-publisher`);

this.worker.use(
http.head('https://raw.githubusercontent.com/rust-lang/crates.io/HEAD/.github/workflows/ci.yml', () => {
return new HttpResponse(null, { status: 500 });
}),
);

await fillIn('[data-test-repository-owner]', 'rust-lang');
await fillIn('[data-test-repository-name]', 'crates.io');
await fillIn('[data-test-workflow-filename]', 'ci.yml');

await waitFor('[data-test-workflow-verification="error"]');

let expected =
'⚠ Could not verify workflow file at https://raw.githubusercontent.com/rust-lang/crates.io/HEAD/.github/workflows/ci.yml (network error)';
assert.dom('[data-test-workflow-verification="error"]').hasText(expected);
});
});
});
Loading