diff --git a/app/adapters/trustpub-gitlab-config.js b/app/adapters/trustpub-gitlab-config.js
new file mode 100644
index 00000000000..1d229cbb6e2
--- /dev/null
+++ b/app/adapters/trustpub-gitlab-config.js
@@ -0,0 +1,7 @@
+import ApplicationAdapter from './application';
+
+export default class TrustpubGitLabConfigAdapter extends ApplicationAdapter {
+ pathForType() {
+ return 'trusted_publishing/gitlab_configs';
+ }
+}
diff --git a/app/controllers/crate/settings/new-trusted-publisher.js b/app/controllers/crate/settings/new-trusted-publisher.js
index de23bd217d6..b7f3506b928 100644
--- a/app/controllers/crate/settings/new-trusted-publisher.js
+++ b/app/controllers/crate/settings/new-trusted-publisher.js
@@ -25,7 +25,7 @@ export default class NewTrustedPublisherController extends Controller {
}
get publishers() {
- return ['GitHub'];
+ return ['GitHub', 'GitLab'];
}
get repository() {
@@ -37,6 +37,8 @@ export default class NewTrustedPublisherController extends Controller {
get verificationUrl() {
if (this.publisher === 'GitHub' && this.namespace && this.project && this.workflow) {
return `https://raw.githubusercontent.com/${this.namespace}/${this.project}/HEAD/.github/workflows/${this.workflow}`;
+ } else if (this.publisher === 'GitLab' && this.namespace && this.project && this.workflow) {
+ return `https://gitlab.com/${this.namespace}/${this.project}/-/raw/HEAD/${this.workflow}`;
}
}
@@ -65,13 +67,24 @@ export default class NewTrustedPublisherController extends Controller {
saveConfigTask = task(async () => {
if (!this.validate()) return;
- let config = this.store.createRecord('trustpub-github-config', {
- crate: this.crate,
- repository_owner: this.namespace,
- repository_name: this.project,
- workflow_filename: this.workflow,
- environment: this.environment || null,
- });
+ let config;
+ if (this.publisher === 'GitHub') {
+ config = this.store.createRecord('trustpub-github-config', {
+ crate: this.crate,
+ repository_owner: this.namespace,
+ repository_name: this.project,
+ workflow_filename: this.workflow,
+ environment: this.environment || null,
+ });
+ } else if (this.publisher === 'GitLab') {
+ config = this.store.createRecord('trustpub-gitlab-config', {
+ crate: this.crate,
+ namespace: this.namespace,
+ project: this.project,
+ workflow_filepath: this.workflow,
+ environment: this.environment || null,
+ });
+ }
try {
// Save the new config on the backend
@@ -106,6 +119,10 @@ export default class NewTrustedPublisherController extends Controller {
return !this.namespaceInvalid && !this.projectInvalid && !this.workflowInvalid;
}
+ @action publisherChanged(event) {
+ this.publisher = event.target.value;
+ }
+
@action resetNamespaceValidation() {
this.namespaceInvalid = false;
}
diff --git a/app/models/trustpub-gitlab-config.js b/app/models/trustpub-gitlab-config.js
new file mode 100644
index 00000000000..76b987f743f
--- /dev/null
+++ b/app/models/trustpub-gitlab-config.js
@@ -0,0 +1,11 @@
+import Model, { attr, belongsTo } from '@ember-data/model';
+
+export default class TrustpubGitLabConfig extends Model {
+ @belongsTo('crate', { async: true, inverse: null }) crate;
+ @attr namespace;
+ @attr namespace_id;
+ @attr project;
+ @attr workflow_filepath;
+ @attr environment;
+ @attr('date') created_at;
+}
diff --git a/app/routes/crate/settings/index.js b/app/routes/crate/settings/index.js
index 4c88ff0930f..5e2ca1bc792 100644
--- a/app/routes/crate/settings/index.js
+++ b/app/routes/crate/settings/index.js
@@ -8,14 +8,16 @@ export default class SettingsIndexRoute extends Route {
let crate = this.modelFor('crate');
let githubConfigs = await this.store.query('trustpub-github-config', { crate: crate.name });
+ let gitlabConfigs = await this.store.query('trustpub-gitlab-config', { crate: crate.name });
- return { crate, githubConfigs };
+ return { crate, githubConfigs, gitlabConfigs };
}
- setupController(controller, { crate, githubConfigs }) {
+ setupController(controller, { crate, githubConfigs, gitlabConfigs }) {
super.setupController(...arguments);
controller.set('crate', crate);
controller.set('githubConfigs', githubConfigs);
+ controller.set('gitlabConfigs', gitlabConfigs);
}
}
diff --git a/app/routes/crate/settings/new-trusted-publisher.js b/app/routes/crate/settings/new-trusted-publisher.js
index 58e87443477..4fdea450d8a 100644
--- a/app/routes/crate/settings/new-trusted-publisher.js
+++ b/app/routes/crate/settings/new-trusted-publisher.js
@@ -27,6 +27,27 @@ export default class NewTrustedPublisherRoute extends Route {
} catch {
// ignore malformed URLs
}
+ } else if (repository && repository.startsWith('https://gitlab.com/')) {
+ controller.publisher = 'GitLab';
+ try {
+ let url = new URL(repository);
+ let pathParts = url.pathname.slice(1).split('/');
+
+ // Find the repository path end (indicated by /-/ for trees/blobs/etc)
+ let repoEndIndex = pathParts.indexOf('-');
+ if (repoEndIndex !== -1) {
+ pathParts = pathParts.slice(0, repoEndIndex);
+ }
+
+ if (pathParts.length >= 2) {
+ // For GitLab, support nested groups: https://gitlab.com/a/b/c
+ // namespace = "a/b", project = "c"
+ controller.namespace = pathParts.slice(0, -1).join('/');
+ controller.project = pathParts.at(-1).replace(/.git$/, '');
+ }
+ } catch {
+ // ignore malformed URLs
+ }
}
}
}
diff --git a/app/serializers/trustpub-gitlab-config.js b/app/serializers/trustpub-gitlab-config.js
new file mode 100644
index 00000000000..0789419b932
--- /dev/null
+++ b/app/serializers/trustpub-gitlab-config.js
@@ -0,0 +1,11 @@
+import ApplicationSerializer from './application';
+
+export default class TrustpubGitLabConfigSerializer extends ApplicationSerializer {
+ modelNameFromPayloadKey() {
+ return 'trustpub-gitlab-config';
+ }
+
+ payloadKeyFromModelName() {
+ return 'gitlab_config';
+ }
+}
diff --git a/app/templates/crate/settings/index.gjs b/app/templates/crate/settings/index.gjs
index e14a078abe1..0095058b929 100644
--- a/app/templates/crate/settings/index.gjs
+++ b/app/templates/crate/settings/index.gjs
@@ -6,6 +6,7 @@ import perform from 'ember-concurrency/helpers/perform';
import preventDefault from 'ember-event-helpers/helpers/prevent-default';
import pageTitle from 'ember-page-title/helpers/page-title';
import not from 'ember-truth-helpers/helpers/not';
+import or from 'ember-truth-helpers/helpers/or';
import CrateHeader from 'crates-io/components/crate-header';
import Tooltip from 'crates-io/components/tooltip';
@@ -165,11 +166,61 @@ import UserAvatar from 'crates-io/components/user-avatar';
>Remove
- {{else}}
+ {{/each}}
+ {{#each @controller.gitlabConfigs as |config|}}
+
+ GitLab
+
+ Repository:
+ {{config.namespace}}/{{config.project}}
+
+ · Namespace ID:
+ {{#if config.namespace_id}}
+ {{config.namespace_id}}
+
+ This is the namespace ID for
+ {{config.namespace}}
+ from the first publish using this configuration. If
+ {{config.namespace}}
+ was recreated on GitLab, this configuration will need to be recreated as well.
+
+ {{else}}
+ (not yet set)
+
+ The namespace ID will be captured from the first publish using this configuration.
+
+ {{/if}}
+
+ Workflow:
+ {{config.workflow_filepath}}
+ {{#if config.environment}}
+ Environment:
+ {{config.environment}}
+ {{/if}}
+
+
+ Remove
+
+
+ {{/each}}
+ {{#unless (or @controller.githubConfigs.length @controller.gitlabConfigs.length)}}
No trusted publishers configured for this crate.
- {{/each}}
+ {{/unless}}
diff --git a/app/templates/crate/settings/new-trusted-publisher.gjs b/app/templates/crate/settings/new-trusted-publisher.gjs
index 45d00ada0d7..02163f58e84 100644
--- a/app/templates/crate/settings/new-trusted-publisher.gjs
+++ b/app/templates/crate/settings/new-trusted-publisher.gjs
@@ -24,6 +24,8 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
disabled={{@controller.saveConfigTask.isRunning}}
class='publisher-select base-input'
data-test-publisher
+ {{on 'change' @controller.publisherChanged}}
+ {{on 'change' (perform @controller.verifyWorkflowTask)}}
>
{{#each @controller.publishers as |publisher|}}
{{publisher}}
@@ -32,7 +34,7 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
{{/let}}
- crates.io currently only supports GitHub, but we are planning to support other platforms in the future.
+ Select the CI/CD platform where your publishing workflow is configured.
@@ -207,6 +209,164 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
{{/let}}
+ {{else if (eq @controller.publisher 'GitLab')}}
+
+
+
+
+
+
+
{{/if}}
diff --git a/e2e/routes/crate/settings.spec.ts b/e2e/routes/crate/settings.spec.ts
index c3d3b6b7c0e..45621852985 100644
--- a/e2e/routes/crate/settings.spec.ts
+++ b/e2e/routes/crate/settings.spec.ts
@@ -59,6 +59,55 @@ test.describe('Route | crate.settings', { tag: '@routes' }, () => {
});
test.describe('Trusted Publishing', () => {
+ test('mixed GitHub and GitLab configs', async ({ msw, page, percy }) => {
+ const { crate } = await prepare(msw);
+
+ // Create GitHub config
+ msw.db.trustpubGithubConfig.create({
+ crate,
+ repository_owner: 'rust-lang',
+ repository_name: 'crates.io',
+ workflow_filename: 'ci.yml',
+ });
+
+ // Create GitLab config
+ msw.db.trustpubGitlabConfig.create({
+ crate,
+ namespace: 'johndoe',
+ namespace_id: '1234',
+ project: 'crates.io',
+ workflow_filepath: '.gitlab-ci.yml',
+ environment: 'production',
+ });
+
+ await page.goto('/crates/foo/settings');
+ await expect(page).toHaveURL('/crates/foo/settings');
+
+ await percy.snapshot();
+
+ // Check that both GitHub and GitLab configs are displayed
+ await expect(page.locator('[data-test-trusted-publishing]')).toBeVisible();
+ await expect(page.locator('[data-test-add-trusted-publisher-button]')).toBeVisible();
+ await expect(page.locator('[data-test-github-config]')).toHaveCount(1);
+ await expect(page.locator('[data-test-gitlab-config]')).toHaveCount(1);
+
+ // Verify GitHub config
+ await expect(page.locator('[data-test-github-config="1"] td:nth-child(1)')).toHaveText('GitHub');
+ let details = page.locator('[data-test-github-config="1"] td:nth-child(2)');
+ await expect(details).toContainText('Repository: rust-lang/crates.io');
+ await expect(details).toContainText('Workflow: ci.yml');
+
+ // Verify GitLab config
+ await expect(page.locator('[data-test-gitlab-config="1"] td:nth-child(1)')).toHaveText('GitLab');
+ details = page.locator('[data-test-gitlab-config="1"] td:nth-child(2)');
+ await expect(details).toContainText('Repository: johndoe/crates.io');
+ await expect(details).toContainText('Namespace ID: 1234');
+ await expect(details).toContainText('Workflow: .gitlab-ci.yml');
+ await expect(details).toContainText('Environment: production');
+
+ await expect(page.locator('[data-test-no-config]')).not.toBeVisible();
+ });
+
test.describe('GitHub', () => {
test('happy path', async ({ msw, page, percy }) => {
const { crate } = await prepare(msw);
@@ -99,8 +148,6 @@ test.describe('Route | crate.settings', { tag: '@routes' }, () => {
await expect(page.locator('[data-test-github-config="2"] [data-test-remove-config-button]')).toBeVisible();
await expect(page.locator('[data-test-no-config]')).not.toBeVisible();
- await percy.snapshot();
-
// Click the remove button
await page.click('[data-test-github-config="2"] [data-test-remove-config-button]');
@@ -143,5 +190,92 @@ test.describe('Route | crate.settings', { tag: '@routes' }, () => {
);
});
});
+
+ test.describe('GitLab', () => {
+ test('happy path', async ({ msw, page, percy }) => {
+ const { crate } = await prepare(msw);
+
+ // Create two GitLab configs for the crate
+ msw.db.trustpubGitlabConfig.create({
+ crate,
+ namespace: 'rust-lang',
+ project: 'crates.io',
+ workflow_filepath: '.gitlab-ci.yml',
+ });
+
+ msw.db.trustpubGitlabConfig.create({
+ crate,
+ namespace: 'johndoe',
+ namespace_id: '1234',
+ project: 'crates.io',
+ workflow_filepath: '.gitlab-ci.yml',
+ environment: 'release',
+ });
+
+ await page.goto('/crates/foo/settings');
+ await expect(page).toHaveURL('/crates/foo/settings');
+
+ await expect(page.locator('[data-test-trusted-publishing]')).toBeVisible();
+ await expect(page.locator('[data-test-add-trusted-publisher-button]')).toBeVisible();
+ await expect(page.locator('[data-test-gitlab-config]')).toHaveCount(2);
+ await expect(page.locator('[data-test-gitlab-config="1"] td:nth-child(1)')).toHaveText('GitLab');
+ let details = page.locator('[data-test-gitlab-config="1"] td:nth-child(2)');
+ await expect(details).toContainText('Repository: rust-lang/crates.io');
+ await expect(details).toContainText('Namespace ID: (not yet set)');
+ await expect(details).toContainText('Workflow: .gitlab-ci.yml');
+ await expect(details).not.toContainText('Environment');
+ await expect(page.locator('[data-test-gitlab-config="1"] [data-test-remove-config-button]')).toBeVisible();
+ await expect(page.locator('[data-test-gitlab-config="2"] td:nth-child(1)')).toHaveText('GitLab');
+ details = page.locator('[data-test-gitlab-config="2"] td:nth-child(2)');
+ await expect(details).toContainText('Repository: johndoe/crates.io');
+ await expect(details).toContainText('Namespace ID: 1234');
+ await expect(details).toContainText('Workflow: .gitlab-ci.yml');
+ await expect(details).toContainText('Environment: release');
+ await expect(page.locator('[data-test-gitlab-config="2"] [data-test-remove-config-button]')).toBeVisible();
+ await expect(page.locator('[data-test-no-config]')).not.toBeVisible();
+
+ // Click the remove button
+ await page.click('[data-test-gitlab-config="2"] [data-test-remove-config-button]');
+
+ // Check that the config is no longer displayed
+ await expect(page.locator('[data-test-gitlab-config]')).toHaveCount(1);
+ details = page.locator('[data-test-gitlab-config="1"] td:nth-child(2)');
+ await expect(details).toContainText('Repository: rust-lang/crates.io');
+ await expect(page.locator('[data-test-notification-message]')).toHaveText(
+ 'Trusted Publishing configuration removed successfully',
+ );
+ });
+
+ test('deletion failure', async ({ msw, page, percy }) => {
+ let { crate } = await prepare(msw);
+
+ // Create a GitLab config for the crate
+ let config = msw.db.trustpubGitlabConfig.create({
+ crate,
+ namespace: 'rust-lang',
+ namespace_id: '1234',
+ project: 'crates.io',
+ workflow_filepath: '.gitlab-ci.yml',
+ environment: 'release',
+ });
+
+ // Mock the server to return an error when trying to delete the config
+ await msw.worker.use(
+ http.delete(`/api/v1/trusted_publishing/gitlab_configs/${config.id}`, () => {
+ return HttpResponse.json({ errors: [{ detail: 'Server error' }] }, { status: 500 });
+ }),
+ );
+
+ await page.goto(`/crates/${crate.name}/settings`);
+ await expect(page).toHaveURL(`/crates/${crate.name}/settings`);
+ await expect(page.locator('[data-test-gitlab-config]')).toHaveCount(1);
+
+ await page.click('[data-test-remove-config-button]');
+ await expect(page.locator('[data-test-gitlab-config]')).toHaveCount(1);
+ await expect(page.locator('[data-test-notification-message]')).toHaveText(
+ 'Failed to remove Trusted Publishing configuration: Server error',
+ );
+ });
+ });
});
});
diff --git a/e2e/routes/crate/settings/new-trusted-publisher.spec.ts b/e2e/routes/crate/settings/new-trusted-publisher.spec.ts
index 00bd619d6f9..6db89074225 100644
--- a/e2e/routes/crate/settings/new-trusted-publisher.spec.ts
+++ b/e2e/routes/crate/settings/new-trusted-publisher.spec.ts
@@ -53,29 +53,57 @@ test.describe('Route | crate.settings.new-trusted-publisher', { tag: '@routes' }
test.describe('prefill', () => {
const testCases = [
{
- name: 'simple https',
+ name: 'GitHub: simple https',
url: 'https://github.com/rust-lang/crates.io',
publisher: 'GitHub',
owner: 'rust-lang',
repo: 'crates.io',
},
{
- name: 'with .git suffix',
+ name: 'GitHub: with .git suffix',
url: 'https://github.com/rust-lang/crates.io.git',
publisher: 'GitHub',
owner: 'rust-lang',
repo: 'crates.io',
},
{
- name: 'with extra path segments',
+ name: 'GitHub: with extra path segments',
url: 'https://github.com/Byron/google-apis-rs/tree/main/gen/privateca1',
publisher: 'GitHub',
owner: 'Byron',
repo: 'google-apis-rs',
},
{
- name: 'non-github url',
+ name: 'GitLab: simple https',
url: 'https://gitlab.com/rust-lang/crates.io',
+ publisher: 'GitLab',
+ owner: 'rust-lang',
+ repo: 'crates.io',
+ },
+ {
+ name: 'GitLab: with .git suffix',
+ url: 'https://gitlab.com/rust-lang/crates.io.git',
+ publisher: 'GitLab',
+ owner: 'rust-lang',
+ repo: 'crates.io',
+ },
+ {
+ name: 'GitLab: with extra path segments',
+ url: 'https://gitlab.com/Byron/google-apis-rs/-/tree/main/gen/privateca1',
+ publisher: 'GitLab',
+ owner: 'Byron',
+ repo: 'google-apis-rs',
+ },
+ {
+ name: 'GitLab: nested groups',
+ url: 'https://gitlab.com/a/b/c',
+ publisher: 'GitLab',
+ owner: 'a/b',
+ repo: 'c',
+ },
+ {
+ name: 'non-github url',
+ url: 'https://example.com/rust-lang/crates.io',
publisher: 'GitHub',
owner: '',
repo: '',
@@ -330,6 +358,237 @@ test.describe('Route | crate.settings.new-trusted-publisher', { tag: '@routes' }
});
test.describe('GitLab', () => {
- // Placeholder for GitLab tests when they are implemented
+ test('happy path', async ({ msw, page }) => {
+ let { crate } = await prepare(msw);
+
+ msw.db.trustpubGitlabConfig.create({
+ crate,
+ namespace: 'johndoe',
+ project: 'crates.io',
+ workflow_filepath: '.gitlab-ci.yml',
+ });
+
+ await page.goto(`/crates/${crate.name}/settings`);
+ await page.click('[data-test-add-trusted-publisher-button]');
+ await expect(page).toHaveURL(`/crates/${crate.name}/settings/new-trusted-publisher`);
+
+ // Check that the form is displayed correctly
+ await expect(page.locator('[data-test-publisher]')).toBeVisible();
+ await expect(page.locator('[data-test-namespace]')).toBeVisible();
+ await expect(page.locator('[data-test-project]')).toBeVisible();
+ await expect(page.locator('[data-test-workflow]')).toBeVisible();
+
+ // Select GitLab from the publisher dropdown
+ await page.selectOption('[data-test-publisher]', 'GitLab');
+
+ // Check that GitLab fields are displayed
+ await expect(page.locator('[data-test-namespace]')).toBeVisible();
+ await expect(page.locator('[data-test-project]')).toBeVisible();
+ await expect(page.locator('[data-test-workflow]')).toBeVisible();
+ await expect(page.locator('[data-test-environment]')).toBeVisible();
+ await expect(page.locator('[data-test-add]')).toBeVisible();
+ await expect(page.locator('[data-test-cancel]')).toBeVisible();
+
+ // Fill in the form
+ await page.fill('[data-test-namespace]', 'rust-lang');
+ await page.fill('[data-test-project]', 'crates.io');
+ await page.fill('[data-test-workflow]', '.gitlab-ci.yml');
+ await page.fill('[data-test-environment]', 'production');
+
+ // Submit the form
+ await page.click('[data-test-add]');
+
+ // Check that we're redirected back to the crate settings page
+ await expect(page).toHaveURL(`/crates/${crate.name}/settings`);
+
+ // Check that the config was created
+ let config = msw.db.trustpubGitlabConfig.findFirst({
+ where: {
+ namespace: { equals: 'rust-lang' },
+ project: { equals: 'crates.io' },
+ workflow_filepath: { equals: '.gitlab-ci.yml' },
+ environment: { equals: 'production' },
+ },
+ });
+ expect(config, 'Config was created').toBeDefined();
+
+ // Check that the success notification is displayed
+ await expect(page.locator('[data-test-notification-message]')).toHaveText(
+ 'Trusted Publishing configuration added successfully',
+ );
+
+ // Check that the config is displayed on the crate settings page
+ await expect(page.locator('[data-test-gitlab-config]')).toHaveCount(2);
+ await expect(page.locator('[data-test-gitlab-config="2"] td:nth-child(1)')).toHaveText('GitLab');
+ let details = page.locator('[data-test-gitlab-config="2"] td:nth-child(2)');
+ await expect(details).toContainText('Repository: rust-lang/crates.io');
+ await expect(details).toContainText('Workflow: .gitlab-ci.yml');
+ await expect(details).toContainText('Environment: production');
+ });
+
+ test('validation errors', 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`);
+
+ // Select GitLab from the publisher dropdown
+ await page.selectOption('[data-test-publisher]', 'GitLab');
+
+ // Submit the form without filling in required fields
+ await page.click('[data-test-add]');
+
+ // Check that validation errors are displayed
+ await expect(page.locator('[data-test-namespace-group] [data-test-error]')).toBeVisible();
+ await expect(page.locator('[data-test-project-group] [data-test-error]')).toBeVisible();
+ await expect(page.locator('[data-test-workflow-group] [data-test-error]')).toBeVisible();
+
+ // Fill in the required fields
+ await page.fill('[data-test-namespace]', 'rust-lang');
+ await page.fill('[data-test-project]', 'crates.io');
+ await page.fill('[data-test-workflow]', '.gitlab-ci.yml');
+
+ // Submit the form
+ await page.click('[data-test-add]');
+
+ // Check that we're redirected back to the crate settings page
+ await expect(page).toHaveURL(`/crates/${crate.name}/settings`);
+ });
+
+ test('loading and error state', async ({ msw, page }) => {
+ let { crate } = await prepare(msw);
+
+ // Mock the server to return an error
+ let deferred = defer();
+ msw.worker.use(http.post('/api/v1/trusted_publishing/gitlab_configs', () => deferred.promise));
+
+ await page.goto(`/crates/${crate.name}/settings/new-trusted-publisher`);
+ await expect(page).toHaveURL(`/crates/${crate.name}/settings/new-trusted-publisher`);
+
+ // Select GitLab from the publisher dropdown
+ await page.selectOption('[data-test-publisher]', 'GitLab');
+
+ // Fill in the form
+ await page.fill('[data-test-namespace]', 'rust-lang');
+ await page.fill('[data-test-project]', 'crates.io');
+ await page.fill('[data-test-workflow]', '.gitlab-ci.yml');
+
+ // Submit the form
+ await page.click('[data-test-add]');
+ await expect(page.locator('[data-test-add] [data-test-spinner]')).toBeVisible();
+ await expect(page.locator('[data-test-publisher]')).toBeDisabled();
+ await expect(page.locator('[data-test-namespace]')).toBeDisabled();
+ await expect(page.locator('[data-test-project]')).toBeDisabled();
+ await expect(page.locator('[data-test-workflow]')).toBeDisabled();
+ await expect(page.locator('[data-test-environment]')).toBeDisabled();
+ await expect(page.locator('[data-test-add]')).toBeDisabled();
+
+ // Resolve the deferred with an error
+ deferred.resolve(HttpResponse.json({ errors: [{ detail: 'Server error' }] }, { status: 500 }));
+
+ // Check that the error notification is displayed
+ await expect(page.locator('[data-test-notification-message]')).toHaveText(
+ 'An error has occurred while adding the Trusted Publishing configuration: Server error',
+ );
+
+ await expect(page.locator('[data-test-publisher]')).toBeEnabled();
+ await expect(page.locator('[data-test-namespace]')).toBeEnabled();
+ await expect(page.locator('[data-test-project]')).toBeEnabled();
+ await expect(page.locator('[data-test-workflow]')).toBeEnabled();
+ await expect(page.locator('[data-test-environment]')).toBeEnabled();
+ await expect(page.locator('[data-test-add]')).toBeEnabled();
+
+ await page.click('[data-test-cancel]');
+ await expect(page).toHaveURL(`/crates/${crate.name}/settings`);
+ await expect(page.locator('[data-test-gitlab-config]')).toHaveCount(0);
+ });
+
+ 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`);
+
+ // Select GitLab from the publisher dropdown
+ await page.selectOption('[data-test-publisher]', 'GitLab');
+
+ await msw.worker.use(
+ http.head('https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml', () => {
+ return new HttpResponse(null, { status: 200 });
+ }),
+ );
+
+ await expect(page.locator('[data-test-workflow-verification="initial"]')).toHaveText(
+ 'The workflow filepath will be verified once all necessary fields are filled.',
+ );
+
+ await page.fill('[data-test-namespace]', 'rust-lang');
+ await page.fill('[data-test-project]', 'crates.io');
+ await page.fill('[data-test-workflow]', '.gitlab-ci.yml');
+
+ await expect(page.locator('[data-test-workflow-verification="success"]')).toBeVisible();
+
+ await expect(page.locator('[data-test-workflow-verification="success"]')).toHaveText(
+ '✓ Workflow file found at https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-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`);
+
+ // Select GitLab from the publisher dropdown
+ await page.selectOption('[data-test-publisher]', 'GitLab');
+
+ await msw.worker.use(
+ http.head('https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/missing.yml', () => {
+ return new HttpResponse(null, { status: 404 });
+ }),
+ );
+
+ await page.fill('[data-test-namespace]', 'rust-lang');
+ await page.fill('[data-test-project]', 'crates.io');
+ await page.fill('[data-test-workflow]', 'missing.yml');
+
+ await expect(page.locator('[data-test-workflow-verification="not-found"]')).toBeVisible();
+
+ await expect(page.locator('[data-test-workflow-verification="not-found"]')).toHaveText(
+ '⚠ Workflow file not found at https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/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`);
+
+ // Select GitLab from the publisher dropdown
+ await page.selectOption('[data-test-publisher]', 'GitLab');
+
+ await msw.worker.use(
+ http.head('https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml', () => {
+ return new HttpResponse(null, { status: 500 });
+ }),
+ );
+
+ await page.fill('[data-test-namespace]', 'rust-lang');
+ await page.fill('[data-test-project]', 'crates.io');
+ await page.fill('[data-test-workflow]', '.gitlab-ci.yml');
+
+ await expect(page.locator('[data-test-workflow-verification="error"]')).toBeVisible();
+
+ await expect(page.locator('[data-test-workflow-verification="error"]')).toHaveText(
+ '⚠ Could not verify workflow file at https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml (network error)',
+ );
+ });
+ });
});
});
diff --git a/packages/crates-io-msw/handlers/gitlab.js b/packages/crates-io-msw/handlers/gitlab.js
new file mode 100644
index 00000000000..b0af61132f0
--- /dev/null
+++ b/packages/crates-io-msw/handlers/gitlab.js
@@ -0,0 +1,7 @@
+import { http, HttpResponse } from 'msw';
+
+export default [
+ http.head('https://gitlab.com/:owner/:project/-/raw/HEAD/:workflow_path', () => {
+ return new HttpResponse(null, { status: 404 });
+ }),
+];
diff --git a/packages/crates-io-msw/index.js b/packages/crates-io-msw/index.js
index c5fd0ec3171..83e7b40fb43 100644
--- a/packages/crates-io-msw/index.js
+++ b/packages/crates-io-msw/index.js
@@ -3,6 +3,7 @@ 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 gitlabHandlers from './handlers/gitlab.js';
import inviteHandlers from './handlers/invites.js';
import keywordHandlers from './handlers/keywords.js';
import metadataHandlers from './handlers/metadata.js';
@@ -35,6 +36,7 @@ export const handlers = [
...cratesHandlers,
...docsRsHandlers,
...githubHandlers,
+ ...gitlabHandlers,
...inviteHandlers,
...keywordHandlers,
...metadataHandlers,
diff --git a/tests/routes/crate/settings-test.js b/tests/routes/crate/settings-test.js
index 50ce9ff5458..858c71ebdc0 100644
--- a/tests/routes/crate/settings-test.js
+++ b/tests/routes/crate/settings-test.js
@@ -63,6 +63,53 @@ module('Route | crate.settings', hooks => {
});
module('Trusted Publishing', function () {
+ test('mixed GitHub and GitLab configs', async function (assert) {
+ const { crate, user } = prepare(this);
+ this.authenticateAs(user);
+
+ // Create GitHub configs
+ this.db.trustpubGithubConfig.create({
+ crate,
+ repository_owner: 'rust-lang',
+ repository_name: 'crates.io',
+ workflow_filename: 'ci.yml',
+ });
+
+ // Create GitLab configs
+ this.db.trustpubGitlabConfig.create({
+ crate,
+ namespace: 'johndoe',
+ namespace_id: '1234',
+ project: 'crates.io',
+ workflow_filepath: '.gitlab-ci.yml',
+ environment: 'production',
+ });
+
+ await visit(`/crates/${crate.name}/settings`);
+ assert.strictEqual(currentURL(), `/crates/${crate.name}/settings`);
+
+ await percySnapshot(assert);
+
+ // Check that both GitHub and GitLab configs are displayed
+ assert.dom('[data-test-trusted-publishing]').exists();
+ assert.dom('[data-test-github-config]').exists({ count: 1 });
+ assert.dom('[data-test-gitlab-config]').exists({ count: 1 });
+
+ // Verify GitHub config
+ assert.dom('[data-test-github-config="1"] td:nth-child(1)').hasText('GitHub');
+ assert.dom('[data-test-github-config="1"] td:nth-child(2)').includesText('Repository: rust-lang/crates.io');
+ assert.dom('[data-test-github-config="1"] td:nth-child(2)').includesText('Workflow: ci.yml');
+
+ // Verify GitLab config
+ assert.dom('[data-test-gitlab-config="1"] td:nth-child(1)').hasText('GitLab');
+ assert.dom('[data-test-gitlab-config="1"] td:nth-child(2)').includesText('Repository: johndoe/crates.io');
+ assert.dom('[data-test-gitlab-config="1"] td:nth-child(2)').includesText('Namespace ID: 1234');
+ assert.dom('[data-test-gitlab-config="1"] td:nth-child(2)').includesText('Workflow: .gitlab-ci.yml');
+ assert.dom('[data-test-gitlab-config="1"] td:nth-child(2)').includesText('Environment: production');
+
+ assert.dom('[data-test-no-config]').doesNotExist();
+ });
+
module('GitHub', function () {
test('happy path', async function (assert) {
const { crate, user } = prepare(this);
@@ -87,8 +134,6 @@ module('Route | crate.settings', hooks => {
await visit(`/crates/${crate.name}/settings`);
assert.strictEqual(currentURL(), `/crates/${crate.name}/settings`);
- await percySnapshot(assert);
-
// Check that the GitHub config is displayed
assert.dom('[data-test-trusted-publishing]').exists();
assert.dom('[data-test-github-config]').exists({ count: 2 });
@@ -144,5 +189,89 @@ module('Route | crate.settings', hooks => {
.hasText('Failed to remove Trusted Publishing configuration: Server error');
});
});
+
+ module('GitLab', function () {
+ test('happy path', async function (assert) {
+ const { crate, user } = prepare(this);
+ this.authenticateAs(user);
+
+ // Create two GitLab configs for the crate
+ this.db.trustpubGitlabConfig.create({
+ crate,
+ namespace: 'rust-lang',
+ project: 'crates.io',
+ workflow_filepath: '.gitlab-ci.yml',
+ });
+
+ this.db.trustpubGitlabConfig.create({
+ crate,
+ namespace: 'johndoe',
+ namespace_id: '1234',
+ project: 'crates.io',
+ workflow_filepath: '.gitlab-ci.yml',
+ environment: 'release',
+ });
+
+ await visit(`/crates/${crate.name}/settings`);
+ assert.strictEqual(currentURL(), `/crates/${crate.name}/settings`);
+
+ // Check that the GitLab config is displayed
+ assert.dom('[data-test-trusted-publishing]').exists();
+ assert.dom('[data-test-gitlab-config]').exists({ count: 2 });
+ assert.dom('[data-test-gitlab-config="1"] td:nth-child(1)').hasText('GitLab');
+ assert.dom('[data-test-gitlab-config="1"] td:nth-child(2)').includesText('Repository: rust-lang/crates.io');
+ assert.dom('[data-test-gitlab-config="1"] td:nth-child(2)').includesText('Namespace ID: (not yet set)');
+ assert.dom('[data-test-gitlab-config="1"] td:nth-child(2)').includesText('Workflow: .gitlab-ci.yml');
+ assert.dom('[data-test-gitlab-config="1"] td:nth-child(2)').doesNotIncludeText('Environment');
+ assert.dom('[data-test-gitlab-config="1"] [data-test-remove-config-button]').exists();
+ assert.dom('[data-test-gitlab-config="2"] td:nth-child(1)').hasText('GitLab');
+ assert.dom('[data-test-gitlab-config="2"] td:nth-child(2)').includesText('Repository: johndoe/crates.io');
+ assert.dom('[data-test-gitlab-config="2"] td:nth-child(2)').includesText('Namespace ID: 1234');
+ assert.dom('[data-test-gitlab-config="2"] td:nth-child(2)').includesText('Workflow: .gitlab-ci.yml');
+ assert.dom('[data-test-gitlab-config="2"] td:nth-child(2)').includesText('Environment: release');
+ assert.dom('[data-test-gitlab-config="2"] [data-test-remove-config-button]').exists();
+ assert.dom('[data-test-no-config]').doesNotExist();
+
+ // Click the remove button
+ await click('[data-test-gitlab-config="2"] [data-test-remove-config-button]');
+
+ // Check that the config is no longer displayed
+ assert.dom('[data-test-gitlab-config]').exists({ count: 1 });
+ assert.dom('[data-test-gitlab-config="1"] td:nth-child(2)').includesText('Repository: rust-lang/crates.io');
+ assert.dom('[data-test-notification-message]').hasText('Trusted Publishing configuration removed successfully');
+ });
+
+ test('deletion failure', async function (assert) {
+ let { crate, user } = prepare(this);
+ this.authenticateAs(user);
+
+ // Create a GitLab config for the crate
+ let config = this.db.trustpubGitlabConfig.create({
+ crate,
+ namespace: 'rust-lang',
+ namespace_id: '1234',
+ project: 'crates.io',
+ workflow_filepath: '.gitlab-ci.yml',
+ environment: 'release',
+ });
+
+ // Mock the server to return an error when trying to delete the config
+ this.worker.use(
+ http.delete(`/api/v1/trusted_publishing/gitlab_configs/${config.id}`, () => {
+ return HttpResponse.json({ errors: [{ detail: 'Server error' }] }, { status: 500 });
+ }),
+ );
+
+ await visit(`/crates/${crate.name}/settings`);
+ assert.strictEqual(currentURL(), `/crates/${crate.name}/settings`);
+ assert.dom('[data-test-gitlab-config]').exists({ count: 1 });
+
+ await click('[data-test-remove-config-button]');
+ assert.dom('[data-test-gitlab-config]').exists({ count: 1 });
+ assert
+ .dom('[data-test-notification-message]')
+ .hasText('Failed to remove Trusted Publishing configuration: Server error');
+ });
+ });
});
});
diff --git a/tests/routes/crate/settings/new-trusted-publisher-test.js b/tests/routes/crate/settings/new-trusted-publisher-test.js
index 97d0331bb20..f562c23bff6 100644
--- a/tests/routes/crate/settings/new-trusted-publisher-test.js
+++ b/tests/routes/crate/settings/new-trusted-publisher-test.js
@@ -63,29 +63,57 @@ module('Route | crate.settings.new-trusted-publisher', hooks => {
module('prefill', function () {
let testCases = [
{
- name: 'simple https',
+ name: 'GitHub: simple https',
url: 'https://github.com/rust-lang/crates.io',
publisher: 'GitHub',
owner: 'rust-lang',
repo: 'crates.io',
},
{
- name: 'with .git suffix',
+ name: 'GitHub: with .git suffix',
url: 'https://github.com/rust-lang/crates.io.git',
publisher: 'GitHub',
owner: 'rust-lang',
repo: 'crates.io',
},
{
- name: 'with extra path segments',
+ name: 'GitHub: with extra path segments',
url: 'https://github.com/Byron/google-apis-rs/tree/main/gen/privateca1',
publisher: 'GitHub',
owner: 'Byron',
repo: 'google-apis-rs',
},
{
- name: 'non-github url',
+ name: 'GitLab: simple https',
url: 'https://gitlab.com/rust-lang/crates.io',
+ publisher: 'GitLab',
+ owner: 'rust-lang',
+ repo: 'crates.io',
+ },
+ {
+ name: 'GitLab: with .git suffix',
+ url: 'https://gitlab.com/rust-lang/crates.io.git',
+ publisher: 'GitLab',
+ owner: 'rust-lang',
+ repo: 'crates.io',
+ },
+ {
+ name: 'GitLab: with extra path segments',
+ url: 'https://gitlab.com/Byron/google-apis-rs/-/tree/main/gen/privateca1',
+ publisher: 'GitLab',
+ owner: 'Byron',
+ repo: 'google-apis-rs',
+ },
+ {
+ name: 'GitLab: nested groups',
+ url: 'https://gitlab.com/a/b/c',
+ publisher: 'GitLab',
+ owner: 'a/b',
+ repo: 'c',
+ },
+ {
+ name: 'non-github url',
+ url: 'https://example.com/rust-lang/crates.io',
publisher: 'GitHub',
owner: '',
repo: '',
@@ -343,6 +371,233 @@ module('Route | crate.settings.new-trusted-publisher', hooks => {
});
module('GitLab', function () {
- // Placeholder for GitLab tests when they are implemented
+ test('happy path', async function (assert) {
+ let { crate } = prepare(this);
+
+ this.db.trustpubGitlabConfig.create({
+ crate,
+ namespace: 'johndoe',
+ project: 'crates.io',
+ workflow_filepath: '.gitlab-ci.yml',
+ });
+
+ await visit(`/crates/${crate.name}/settings`);
+ await click('[data-test-add-trusted-publisher-button]');
+ assert.strictEqual(currentURL(), `/crates/${crate.name}/settings/new-trusted-publisher`);
+
+ // Check that the form is displayed correctly
+ assert.dom('[data-test-publisher]').exists();
+ assert.dom('[data-test-namespace]').exists();
+ assert.dom('[data-test-project]').exists();
+ assert.dom('[data-test-workflow]').exists();
+
+ // Select GitLab from the publisher dropdown
+ await fillIn('[data-test-publisher]', 'GitLab');
+
+ // Check that GitLab fields are displayed
+ assert.dom('[data-test-namespace]').exists();
+ assert.dom('[data-test-project]').exists();
+ assert.dom('[data-test-workflow]').exists();
+ assert.dom('[data-test-environment]').exists();
+ assert.dom('[data-test-add]').exists();
+ assert.dom('[data-test-cancel]').exists();
+
+ // Fill in the form
+ await fillIn('[data-test-namespace]', 'rust-lang');
+ await fillIn('[data-test-project]', 'crates.io');
+ await fillIn('[data-test-workflow]', '.gitlab-ci.yml');
+ await fillIn('[data-test-environment]', 'production');
+
+ // Submit the form
+ await click('[data-test-add]');
+
+ // Check that we're redirected back to the crate settings page
+ assert.strictEqual(currentURL(), `/crates/${crate.name}/settings`);
+
+ // Check that the config was created
+ let config = this.db.trustpubGitlabConfig.findFirst({
+ where: {
+ namespace: { equals: 'rust-lang' },
+ project: { equals: 'crates.io' },
+ workflow_filepath: { equals: '.gitlab-ci.yml' },
+ environment: { equals: 'production' },
+ },
+ });
+ assert.ok(config, 'Config was created');
+
+ // Check that the success notification is displayed
+ assert.dom('[data-test-notification-message]').hasText('Trusted Publishing configuration added successfully');
+
+ // Check that the config is displayed on the crate settings page
+ assert.dom('[data-test-gitlab-config]').exists({ count: 2 });
+ assert.dom('[data-test-gitlab-config="2"] td:nth-child(1)').hasText('GitLab');
+ assert.dom('[data-test-gitlab-config="2"] td:nth-child(2)').includesText('Repository: rust-lang/crates.io');
+ assert.dom('[data-test-gitlab-config="2"] td:nth-child(2)').includesText('Workflow: .gitlab-ci.yml');
+ assert.dom('[data-test-gitlab-config="2"] td:nth-child(2)').includesText('Environment: production');
+ });
+
+ test('validation errors', 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`);
+
+ // Select GitLab from the publisher dropdown
+ await fillIn('[data-test-publisher]', 'GitLab');
+
+ // Submit the form without filling in required fields
+ await click('[data-test-add]');
+
+ // Check that validation errors are displayed
+ assert.dom('[data-test-namespace-group] [data-test-error]').exists();
+ assert.dom('[data-test-project-group] [data-test-error]').exists();
+ assert.dom('[data-test-workflow-group] [data-test-error]').exists();
+
+ // Fill in the required fields
+ await fillIn('[data-test-namespace]', 'rust-lang');
+ await fillIn('[data-test-project]', 'crates.io');
+ await fillIn('[data-test-workflow]', '.gitlab-ci.yml');
+
+ // Submit the form
+ await click('[data-test-add]');
+
+ // Check that we're redirected back to the crate settings page
+ assert.strictEqual(currentURL(), `/crates/${crate.name}/settings`);
+ });
+
+ test('loading and error state', async function (assert) {
+ let { crate } = prepare(this);
+
+ // Mock the server to return an error
+ let deferred = defer();
+ this.worker.use(http.post('/api/v1/trusted_publishing/gitlab_configs', () => deferred.promise));
+
+ await visit(`/crates/${crate.name}/settings/new-trusted-publisher`);
+ assert.strictEqual(currentURL(), `/crates/${crate.name}/settings/new-trusted-publisher`);
+
+ // Select GitLab from the publisher dropdown
+ await fillIn('[data-test-publisher]', 'GitLab');
+
+ // Fill in the form
+ await fillIn('[data-test-namespace]', 'rust-lang');
+ await fillIn('[data-test-project]', 'crates.io');
+ await fillIn('[data-test-workflow]', '.gitlab-ci.yml');
+
+ // Submit the form
+ let clickPromise = click('[data-test-add]');
+ await waitFor('[data-test-add] [data-test-spinner]');
+ assert.dom('[data-test-publisher]').isDisabled();
+ assert.dom('[data-test-namespace]').isDisabled();
+ assert.dom('[data-test-project]').isDisabled();
+ assert.dom('[data-test-workflow]').isDisabled();
+ assert.dom('[data-test-environment]').isDisabled();
+ assert.dom('[data-test-add]').isDisabled();
+
+ // Resolve the deferred with an error
+ deferred.resolve(HttpResponse.json({ errors: [{ detail: 'Server error' }] }, { status: 500 }));
+ await clickPromise;
+
+ // Check that the error notification is displayed
+ assert
+ .dom('[data-test-notification-message]')
+ .hasText('An error has occurred while adding the Trusted Publishing configuration: Server error');
+
+ assert.dom('[data-test-publisher]').isEnabled();
+ assert.dom('[data-test-namespace]').isEnabled();
+ assert.dom('[data-test-project]').isEnabled();
+ assert.dom('[data-test-workflow]').isEnabled();
+ assert.dom('[data-test-environment]').isEnabled();
+ assert.dom('[data-test-add]').isEnabled();
+
+ await click('[data-test-cancel]');
+ assert.strictEqual(currentURL(), `/crates/${crate.name}/settings`);
+ assert.dom('[data-test-gitlab-config]').exists({ count: 0 });
+ });
+
+ 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`);
+
+ // Select GitLab from the publisher dropdown
+ await fillIn('[data-test-publisher]', 'GitLab');
+
+ this.worker.use(
+ http.head('https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml', () => {
+ return new HttpResponse(null, { status: 200 });
+ }),
+ );
+
+ assert
+ .dom('[data-test-workflow-verification="initial"]')
+ .hasText('The workflow filepath will be verified once all necessary fields are filled.');
+
+ await fillIn('[data-test-namespace]', 'rust-lang');
+ await fillIn('[data-test-project]', 'crates.io');
+ await fillIn('[data-test-workflow]', '.gitlab-ci.yml');
+
+ await waitFor('[data-test-workflow-verification="success"]');
+
+ let expected = '✓ Workflow file found at https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-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`);
+
+ // Select GitLab from the publisher dropdown
+ await fillIn('[data-test-publisher]', 'GitLab');
+
+ this.worker.use(
+ http.head('https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/missing.yml', () => {
+ return new HttpResponse(null, { status: 404 });
+ }),
+ );
+
+ await fillIn('[data-test-namespace]', 'rust-lang');
+ await fillIn('[data-test-project]', 'crates.io');
+ await fillIn('[data-test-workflow]', 'missing.yml');
+
+ await waitFor('[data-test-workflow-verification="not-found"]');
+
+ let expected = '⚠ Workflow file not found at https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/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`);
+
+ // Select GitLab from the publisher dropdown
+ await fillIn('[data-test-publisher]', 'GitLab');
+
+ this.worker.use(
+ http.head('https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml', () => {
+ return new HttpResponse(null, { status: 500 });
+ }),
+ );
+
+ await fillIn('[data-test-namespace]', 'rust-lang');
+ await fillIn('[data-test-project]', 'crates.io');
+ await fillIn('[data-test-workflow]', '.gitlab-ci.yml');
+
+ await waitFor('[data-test-workflow-verification="error"]');
+
+ let expected =
+ '⚠ Could not verify workflow file at https://gitlab.com/rust-lang/crates.io/-/raw/HEAD/.gitlab-ci.yml (network error)';
+ assert.dom('[data-test-workflow-verification="error"]').hasText(expected);
+ });
+ });
});
});