Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions app/lib/frontend/handlers/experimental.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const _publicFlags = <PublicFlag>{

final _allFlags = <String>{
'dark-as-default',
'manual-publishing',
..._publicFlags.map((x) => x.name),
};

Expand Down Expand Up @@ -88,6 +89,8 @@ class ExperimentalFlags {

bool get isDarkModeDefault => isEnabled('dark-as-default');

bool get isManualPublishingConfigAvailable => isEnabled('manual-publishing');

String encodedAsCookie() => _enabled.join(':');

@override
Expand Down
26 changes: 26 additions & 0 deletions app/lib/frontend/templates/views/pkg/admin_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.

import 'package:_pub_shared/data/package_api.dart';
import 'package:pub_dev/frontend/request_context.dart';

import '../../../../account/models.dart';
import '../../../../package/models.dart';
Expand Down Expand Up @@ -42,6 +43,8 @@ d.Node packageAdminPageNode({
),
],
),
if (requestContext.experimentalFlags.isManualPublishingConfigAvailable)
TocNode('Manual publishing', href: '#manual-publishing'),
TocNode('Version retraction', href: '#version-retraction'),
]),
d.a(name: 'ownership'),
Expand Down Expand Up @@ -227,6 +230,8 @@ d.Node packageAdminPageNode({
),
],
_automatedPublishing(package),
if (requestContext.experimentalFlags.isManualPublishingConfigAvailable)
_manualPublishing(package),
d.a(name: 'version-retraction'),
d.h2(text: 'Version retraction'),
d.div(
Expand Down Expand Up @@ -453,6 +458,27 @@ d.Node _automatedPublishing(Package package) {
]);
}

d.Node _manualPublishing(Package package) {
final manual = package.automatedPublishing?.manualConfig;
return d.fragment([
d.a(name: 'manual-publishing'),
d.h2(text: 'Manual publishing'),
d.markdown(
'The manual publishing of new versions using the `pub` tool is enabled by default in all packages. '
'Disabling it may protect the package from accidental publishing events when the package is otherwise using '
'automated publishing, or in other cases, is discontinued.',
),
d.div(
classes: ['-pub-form-checkbox-row'],
child: material.checkbox(
id: '-admin-is-manual-publishing-disabled',
label: 'Disable manual publishing',
checked: manual?.isDisabled ?? false,
),
),
]);
}

d.Node _exampleGitHubWorkflow(GitHubPublishingConfig github) {
final expandedTagPattern = (github.tagPattern ?? '{{version}}').replaceAll(
'{{version}}',
Expand Down
23 changes: 19 additions & 4 deletions app/lib/package/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,8 @@ class PackageBackend {
final p = await tx.lookupValue<Package>(pkg.key);
final githubConfig = body.github;
final gcpConfig = body.gcp;
final manualConfig = body.manual;

if (githubConfig != null) {
final isEnabled = githubConfig.isEnabled;

Expand All @@ -648,7 +650,9 @@ class PackageBackend {
final repository = githubConfig.repository?.trim() ?? '';
githubConfig.repository = repository.isEmpty ? null : repository;
final tagPattern = githubConfig.tagPattern?.trim() ?? '';
verifyTagPattern(tagPattern: tagPattern);
if (isEnabled) {
verifyTagPattern(tagPattern: tagPattern);
}
githubConfig.tagPattern = tagPattern.isEmpty ? null : tagPattern;
final environment = githubConfig.environment?.trim() ?? '';
githubConfig.environment = environment.isEmpty ? null : environment;
Expand Down Expand Up @@ -726,9 +730,14 @@ class PackageBackend {
}

// finalize changes
p.automatedPublishing ??= AutomatedPublishing();
p.automatedPublishing!.githubConfig = githubConfig;
p.automatedPublishing!.gcpConfig = gcpConfig;
final automatedPublishing = p.automatedPublishing ??=
AutomatedPublishing();
automatedPublishing.githubConfig =
githubConfig ?? automatedPublishing.githubConfig;
automatedPublishing.gcpConfig =
gcpConfig ?? automatedPublishing.gcpConfig;
automatedPublishing.manualConfig =
manualConfig ?? automatedPublishing.manualConfig;

p.updated = clock.now().toUtc();
tx.insert(p);
Expand All @@ -742,6 +751,7 @@ class PackageBackend {
return api.AutomatedPublishingConfig(
github: p.automatedPublishing!.githubConfig,
gcp: p.automatedPublishing!.gcpConfig,
manual: p.automatedPublishing!.manualConfig,
);
});
}
Expand Down Expand Up @@ -1606,6 +1616,11 @@ class PackageBackend {
}
if (agent is AuthenticatedUser &&
await packageBackend.isPackageAdmin(package, agent.user.userId)) {
final isDisabled =
package.automatedPublishing?.manualConfig?.isDisabled ?? false;
if (isDisabled) {
throw AuthorizationException.manualPublishingDisabled();
}
return;
}
if (agent is AuthenticatedGitHubAction) {
Expand Down
2 changes: 2 additions & 0 deletions app/lib/package/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -460,12 +460,14 @@ class AutomatedPublishing {
GitHubPublishingLock? githubLock;
GcpPublishingConfig? gcpConfig;
GcpPublishingLock? gcpLock;
ManualPublishingConfig? manualConfig;

AutomatedPublishing({
this.githubConfig,
this.githubLock,
this.gcpConfig,
this.gcpLock,
this.manualConfig,
});

factory AutomatedPublishing.fromJson(Map<String, dynamic> json) =>
Expand Down
6 changes: 6 additions & 0 deletions app/lib/package/models.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions app/lib/shared/exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,12 @@ class AuthorizationException extends ResponseException {
'The calling service account is not allowed to publish, because: $reason.\nSee https://dart.dev/go/publishing-with-service-account',
);

/// Signaling that the manual publishing was disabled and cannot be authorized.
factory AuthorizationException.manualPublishingDisabled() =>
AuthorizationException._(
'The manual publishing with the `pub` tool is disabled on the package admin page.',
);

@override
String toString() => '$code: $message'; // used by package:pub_server
}
Expand Down
74 changes: 74 additions & 0 deletions app/test/package/automated_publishing_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -309,5 +309,79 @@ void main() {
);
},
);

testWithProfile(
'partial settings do not override the other',
fn: () async {
final client = await createFakeAuthPubApiClient(
email: adminAtPubDevEmail,
);

Future<void> update({
GitHubPublishingConfig? github,
GcpPublishingConfig? gcp,
ManualPublishingConfig? manual,
required Map<String, dynamic> expected,
}) async {
final rs = await client.setAutomatedPublishing(
'oxygen',
AutomatedPublishingConfig(github: github, gcp: gcp, manual: manual),
);
expect(rs.toJson(), expected);
}

await update(
manual: ManualPublishingConfig(isDisabled: false),
expected: {
'manual': {'isDisabled': false},
},
);

await update(
github: GitHubPublishingConfig(isEnabled: false),
expected: {
'github': {
'isEnabled': false,
'requireEnvironment': false,
'isPushEventEnabled': true,
'isWorkflowDispatchEventEnabled': false,
},
'manual': {'isDisabled': false},
},
);

await update(
manual: ManualPublishingConfig(isDisabled: true),
expected: {
'github': {
'isEnabled': false,
'requireEnvironment': false,
'isPushEventEnabled': true,
'isWorkflowDispatchEventEnabled': false,
},
'manual': {'isDisabled': true},
},
);

await update(
github: GitHubPublishingConfig(
isEnabled: true,
tagPattern: '{{version}}',
repository: 'user/repo',
),
expected: {
'github': {
'isEnabled': true,
'repository': 'user/repo',
'tagPattern': '{{version}}',
'requireEnvironment': false,
'isPushEventEnabled': true,
'isWorkflowDispatchEventEnabled': false,
},
'manual': {'isDisabled': true},
},
);
},
);
});
}
32 changes: 32 additions & 0 deletions app/test/package/upload_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,38 @@ void main() {
);
});

group('Manual publishing overrides', () {
testWithProfile(
'manual publishing disabled',
fn: () async {
await withFakeAuthRetryPubApiClient(email: adminAtPubDevEmail, (
client,
) async {
await client.setAutomatedPublishing(
'oxygen',
AutomatedPublishingConfig(
manual: ManualPublishingConfig(isDisabled: true),
),
);
});

final bytes = await packageArchiveBytes(
pubspecContent: generatePubspecYaml('oxygen', '2.2.0'),
);
final rs = createPubApiClient(
authToken: adminClientToken,
).uploadPackageBytes(bytes);
await expectApiException(
rs,
status: 403,
code: 'InsufficientPermissions',
message:
'The manual publishing with the `pub` tool is disabled on the package admin page.',
);
},
);
});

group('Uploading with service account', () {
testWithProfile(
'service account cannot upload new package',
Expand Down
15 changes: 14 additions & 1 deletion pkg/_pub_shared/lib/data/package_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ class PkgOptions {
class AutomatedPublishingConfig {
final GitHubPublishingConfig? github;
final GcpPublishingConfig? gcp;
final ManualPublishingConfig? manual;

AutomatedPublishingConfig({this.github, this.gcp});
AutomatedPublishingConfig({this.github, this.gcp, this.manual});

factory AutomatedPublishingConfig.fromJson(Map<String, dynamic> json) =>
_$AutomatedPublishingConfigFromJson(json);
Expand Down Expand Up @@ -120,6 +121,18 @@ class GcpPublishingConfig {
Map<String, dynamic> toJson() => _$GcpPublishingConfigToJson(this);
}

@JsonSerializable(includeIfNull: false, explicitToJson: true)
class ManualPublishingConfig {
bool isDisabled;

ManualPublishingConfig({this.isDisabled = false});

factory ManualPublishingConfig.fromJson(Map<String, dynamic> json) =>
_$ManualPublishingConfigFromJson(json);

Map<String, dynamic> toJson() => _$ManualPublishingConfigToJson(this);
}

@JsonSerializable()
class VersionOptions {
final bool? isRetracted;
Expand Down
12 changes: 12 additions & 0 deletions pkg/_pub_shared/lib/data/package_api.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pkg/pub_integration/lib/src/test_browser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,9 @@ extension PageExt on Page {
}

/// Returns the [property] value of the first element by [selector].
Future<String> propertyValue(String selector, String property) async {
Future<T> propertyValue<T>(String selector, String property) async {
final h = await $(selector);
return await h.propertyValue(property);
return await h.propertyValue<T>(property);
}
}

Expand Down
16 changes: 16 additions & 0 deletions pkg/pub_integration/test/pkg_admin_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ void main() {
expect(value, githubRepository);
});

// disable manual publishing
await user.withBrowserPage((page) async {
await page.gotoOrigin('/experimental?manual-publishing=1');
await page.gotoOrigin('/packages/test_pkg/admin');

await page.waitAndClick('#-admin-is-manual-publishing-disabled');
await page.waitAndClickOnDialogOk(waitForOneResponse: true);
await page.reload();

final value = await page.propertyValue(
'#-admin-is-manual-publishing-disabled',
'checked',
);
expect(value, true);
});

// visit activity log page
await user.withBrowserPage((page) async {
await page.gotoOrigin('/packages/test_pkg/activity-log');
Expand Down
Loading
Loading