Skip to content
Open
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
7 changes: 7 additions & 0 deletions app/adapters/trustpub-gitlab-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import ApplicationAdapter from './application';

export default class TrustpubGitLabConfigAdapter extends ApplicationAdapter {
pathForType() {
return 'trusted_publishing/gitlab_configs';
}
}
33 changes: 25 additions & 8 deletions app/controllers/crate/settings/new-trusted-publisher.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default class NewTrustedPublisherController extends Controller {
}

get publishers() {
return ['GitHub'];
return ['GitHub', 'GitLab'];
}

get repository() {
Expand All @@ -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}`;
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
11 changes: 11 additions & 0 deletions app/models/trustpub-gitlab-config.js
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 4 additions & 2 deletions app/routes/crate/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
21 changes: 21 additions & 0 deletions app/routes/crate/settings/new-trusted-publisher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
}
11 changes: 11 additions & 0 deletions app/serializers/trustpub-gitlab-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import ApplicationSerializer from './application';

export default class TrustpubGitLabConfigSerializer extends ApplicationSerializer {
modelNameFromPayloadKey() {
return 'trustpub-gitlab-config';
}

payloadKeyFromModelName() {
return 'gitlab_config';
}
}
55 changes: 53 additions & 2 deletions app/templates/crate/settings/index.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -165,11 +166,61 @@ import UserAvatar from 'crates-io/components/user-avatar';
>Remove</button>
</td>
</tr>
{{else}}
{{/each}}
{{#each @controller.gitlabConfigs as |config|}}
<tr data-test-gitlab-config={{config.id}}>
<td>GitLab</td>
<td class='details'>
<strong>Repository:</strong>
<a
href='https://gitlab.com/{{config.namespace}}/{{config.project}}'
target='_blank'
rel='noopener noreferrer'
>{{config.namespace}}/{{config.project}}</a>
<span class='owner-id'>
· Namespace ID:
{{#if config.namespace_id}}
{{config.namespace_id}}
<Tooltip>
This is the namespace ID for
<strong>{{config.namespace}}</strong>
from the first publish using this configuration. If
<strong>{{config.namespace}}</strong>
was recreated on GitLab, this configuration will need to be recreated as well.
</Tooltip>
{{else}}
(not yet set)
<Tooltip>
The namespace ID will be captured from the first publish using this configuration.
</Tooltip>
{{/if}}
</span><br />
<strong>Workflow:</strong>
<a
href='https://gitlab.com/{{config.namespace}}/{{config.project}}/-/blob/HEAD/{{config.workflow_filepath}}'
target='_blank'
rel='noopener noreferrer'
>{{config.workflow_filepath}}</a><br />
{{#if config.environment}}
<strong>Environment:</strong>
{{config.environment}}
{{/if}}
</td>
<td class='actions'>
<button
type='button'
class='button button--small'
data-test-remove-config-button
{{on 'click' (perform @controller.removeConfigTask config)}}
>Remove</button>
</td>
</tr>
{{/each}}
{{#unless (or @controller.githubConfigs.length @controller.gitlabConfigs.length)}}
<tr data-test-no-config>
<td colspan='3'>No trusted publishers configured for this crate.</td>
</tr>
{{/each}}
{{/unless}}
</tbody>
</table>

Expand Down
162 changes: 161 additions & 1 deletion app/templates/crate/settings/new-trusted-publisher.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -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|}}
<option value={{publisher}} selected={{eq @controller.publisher publisher}}>{{publisher}}</option>
Expand All @@ -32,7 +34,7 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
{{/let}}

<div class='note'>
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we want to continue to hint that we may support other platforms in the future here. I'm fine with this as it is, but just flagging that we're removing that from here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while supporting more platforms would be good, I don't think I can justify spending more time on adding additional ones, so that implementation would have to be done by the community.

since the previous statement said "planning to support other platforms in the future" I think it's probably fine to remove that part now that we do support an additional platform.

</div>
</div>

Expand Down Expand Up @@ -207,6 +209,164 @@ import LoadingSpinner from 'crates-io/components/loading-spinner';
</div>
{{/let}}
</div>
{{else if (eq @controller.publisher 'GitLab')}}
<div class='form-group' data-test-namespace-group>
{{#let (uniqueId) as |id|}}
<label for={{id}} class='form-group-name'>Namespace</label>

<Input
id={{id}}
@type='text'
@value={{@controller.namespace}}
disabled={{@controller.saveConfigTask.isRunning}}
aria-required='true'
aria-invalid={{if @controller.namespaceInvalid 'true' 'false'}}
class='input base-input'
data-test-namespace
{{autoFocus}}
{{on 'input' @controller.resetNamespaceValidation}}
{{on 'input' (perform @controller.verifyWorkflowTask)}}
/>

{{#if @controller.namespaceInvalid}}
<div class='form-group-error' data-test-error>
Please enter a namespace.
</div>
{{else}}
<div class='note'>
The GitLab group name or GitLab username that owns the project.
</div>
{{/if}}
{{/let}}
</div>

<div class='form-group' data-test-project-group>
{{#let (uniqueId) as |id|}}
<label for={{id}} class='form-group-name'>Project</label>

<Input
id={{id}}
@type='text'
@value={{@controller.project}}
disabled={{@controller.saveConfigTask.isRunning}}
aria-required='true'
aria-invalid={{if @controller.projectInvalid 'true' 'false'}}
class='input base-input'
data-test-project
{{on 'input' @controller.resetProjectValidation}}
{{on 'input' (perform @controller.verifyWorkflowTask)}}
/>

{{#if @controller.projectInvalid}}
<div class='form-group-error' data-test-error>
Please enter a project name.
</div>
{{else}}
<div class='note'>
The name of the GitLab project that contains the publishing workflow.
</div>
{{/if}}
{{/let}}
</div>

<div class='form-group' data-test-workflow-group>
{{#let (uniqueId) as |id|}}
<label for={{id}} class='form-group-name'>Workflow filepath</label>

<Input
id={{id}}
@type='text'
@value={{@controller.workflow}}
disabled={{@controller.saveConfigTask.isRunning}}
aria-required='true'
aria-invalid={{if @controller.workflowInvalid 'true' 'false'}}
class='input base-input'
data-test-workflow
{{on 'input' @controller.resetWorkflowValidation}}
{{on 'input' (perform @controller.verifyWorkflowTask)}}
/>

{{#if @controller.workflowInvalid}}
<div class='form-group-error' data-test-error>
Please enter a workflow filepath.
</div>
{{else}}
<div class='note'>
The filepath to the GitLab CI configuration file, relative to the
{{#if @controller.repository}}<a
href='https://gitlab.com/{{@controller.repository}}/'
target='_blank'
rel='noopener noreferrer'
>{{@controller.repository}}</a>
{{/if}}
repository{{unless @controller.repository ' configured above'}}
root. For example:
<code>.gitlab-ci.yml</code>
or
<code>ci/publish.yml</code>.
</div>
{{/if}}

{{#if (not @controller.verificationUrl)}}
<div class='workflow-verification' data-test-workflow-verification='initial'>
The workflow filepath 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>

<div class='form-group' data-test-environment-group>
{{#let (uniqueId) as |id|}}
<label for={{id}} class='form-group-name'>Environment name (optional)</label>

<Input
id={{id}}
@type='text'
@value={{@controller.environment}}
disabled={{@controller.saveConfigTask.isRunning}}
class='input base-input'
data-test-environment
/>

<div class='note'>
The name of the
<a href='https://docs.gitlab.com/ee/ci/environments/'>GitLab environment</a>
that the above workflow uses for publishing. This should be configured in the project settings. A dedicated
publishing environment is not required, but is
<strong>strongly recommended</strong>, especially if your project has maintainers with merge access who
should not have crates.io publishing access.
</div>
{{/let}}
</div>
{{/if}}

<div class='buttons'>
Expand Down
Loading
Loading