From c86eae00c5a94572530413dc64e695d1684f7779 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Thu, 9 Oct 2025 10:21:19 +0200 Subject: [PATCH 1/9] Option to disable manual publishing --- app/lib/frontend/handlers/experimental.dart | 3 + .../templates/views/pkg/admin_page.dart | 26 +++++++ app/lib/package/backend.dart | 23 +++++- app/lib/package/models.dart | 2 + app/lib/package/models.g.dart | 6 ++ app/lib/shared/exceptions.dart | 6 ++ .../package/automated_publishing_test.dart | 74 +++++++++++++++++++ app/test/package/upload_test.dart | 32 ++++++++ pkg/_pub_shared/lib/data/package_api.dart | 15 +++- pkg/_pub_shared/lib/data/package_api.g.dart | 12 +++ pkg/pub_integration/lib/src/test_browser.dart | 4 +- .../test/pkg_admin_page_test.dart | 16 ++++ pkg/web_app/lib/src/admin_pages.dart | 33 +++++++++ 13 files changed, 245 insertions(+), 7 deletions(-) diff --git a/app/lib/frontend/handlers/experimental.dart b/app/lib/frontend/handlers/experimental.dart index d745713e2b..c99f4483e2 100644 --- a/app/lib/frontend/handlers/experimental.dart +++ b/app/lib/frontend/handlers/experimental.dart @@ -14,6 +14,7 @@ const _publicFlags = { final _allFlags = { 'dark-as-default', + 'manual-publishing', ..._publicFlags.map((x) => x.name), }; @@ -88,6 +89,8 @@ class ExperimentalFlags { bool get isDarkModeDefault => isEnabled('dark-as-default'); + bool get isManualPublishingConfigAvailable => isEnabled('manual-publishing'); + String encodedAsCookie() => _enabled.join(':'); @override diff --git a/app/lib/frontend/templates/views/pkg/admin_page.dart b/app/lib/frontend/templates/views/pkg/admin_page.dart index 9f86a01776..449a04dfe5 100644 --- a/app/lib/frontend/templates/views/pkg/admin_page.dart +++ b/app/lib/frontend/templates/views/pkg/admin_page.dart @@ -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'; @@ -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'), @@ -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( @@ -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}}', diff --git a/app/lib/package/backend.dart b/app/lib/package/backend.dart index 3c07806119..b53911ed78 100644 --- a/app/lib/package/backend.dart +++ b/app/lib/package/backend.dart @@ -635,6 +635,8 @@ class PackageBackend { final p = await tx.lookupValue(pkg.key); final githubConfig = body.github; final gcpConfig = body.gcp; + final manualConfig = body.manual; + if (githubConfig != null) { final isEnabled = githubConfig.isEnabled; @@ -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; @@ -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); @@ -742,6 +751,7 @@ class PackageBackend { return api.AutomatedPublishingConfig( github: p.automatedPublishing!.githubConfig, gcp: p.automatedPublishing!.gcpConfig, + manual: p.automatedPublishing!.manualConfig, ); }); } @@ -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) { diff --git a/app/lib/package/models.dart b/app/lib/package/models.dart index a93ad39f91..d835998904 100644 --- a/app/lib/package/models.dart +++ b/app/lib/package/models.dart @@ -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 json) => diff --git a/app/lib/package/models.g.dart b/app/lib/package/models.g.dart index b49793ac16..15c3103151 100644 --- a/app/lib/package/models.g.dart +++ b/app/lib/package/models.g.dart @@ -54,6 +54,11 @@ AutomatedPublishing _$AutomatedPublishingFromJson(Map json) => gcpLock: json['gcpLock'] == null ? null : GcpPublishingLock.fromJson(json['gcpLock'] as Map), + manualConfig: json['manualConfig'] == null + ? null + : ManualPublishingConfig.fromJson( + json['manualConfig'] as Map, + ), ); Map _$AutomatedPublishingToJson( @@ -63,6 +68,7 @@ Map _$AutomatedPublishingToJson( 'githubLock': ?instance.githubLock?.toJson(), 'gcpConfig': ?instance.gcpConfig?.toJson(), 'gcpLock': ?instance.gcpLock?.toJson(), + 'manualConfig': ?instance.manualConfig?.toJson(), }; GitHubPublishingLock _$GitHubPublishingLockFromJson( diff --git a/app/lib/shared/exceptions.dart b/app/lib/shared/exceptions.dart index 3e24c32376..56cf83ccff 100644 --- a/app/lib/shared/exceptions.dart +++ b/app/lib/shared/exceptions.dart @@ -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 } diff --git a/app/test/package/automated_publishing_test.dart b/app/test/package/automated_publishing_test.dart index 0ec8c7f5e4..1f8227649c 100644 --- a/app/test/package/automated_publishing_test.dart +++ b/app/test/package/automated_publishing_test.dart @@ -309,5 +309,79 @@ void main() { ); }, ); + + testWithProfile( + 'partial settings do not override the other', + fn: () async { + final client = await createFakeAuthPubApiClient( + email: adminAtPubDevEmail, + ); + + Future update({ + GitHubPublishingConfig? github, + GcpPublishingConfig? gcp, + ManualPublishingConfig? manual, + required Map 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}, + }, + ); + }, + ); }); } diff --git a/app/test/package/upload_test.dart b/app/test/package/upload_test.dart index 91c8eff79a..6dc4320c15 100644 --- a/app/test/package/upload_test.dart +++ b/app/test/package/upload_test.dart @@ -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', diff --git a/pkg/_pub_shared/lib/data/package_api.dart b/pkg/_pub_shared/lib/data/package_api.dart index 0c4145930a..2066e0a7dc 100644 --- a/pkg/_pub_shared/lib/data/package_api.dart +++ b/pkg/_pub_shared/lib/data/package_api.dart @@ -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 json) => _$AutomatedPublishingConfigFromJson(json); @@ -120,6 +121,18 @@ class GcpPublishingConfig { Map toJson() => _$GcpPublishingConfigToJson(this); } +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class ManualPublishingConfig { + bool isDisabled; + + ManualPublishingConfig({this.isDisabled = false}); + + factory ManualPublishingConfig.fromJson(Map json) => + _$ManualPublishingConfigFromJson(json); + + Map toJson() => _$ManualPublishingConfigToJson(this); +} + @JsonSerializable() class VersionOptions { final bool? isRetracted; diff --git a/pkg/_pub_shared/lib/data/package_api.g.dart b/pkg/_pub_shared/lib/data/package_api.g.dart index dcfd9902af..1ca91ffb7e 100644 --- a/pkg/_pub_shared/lib/data/package_api.g.dart +++ b/pkg/_pub_shared/lib/data/package_api.g.dart @@ -38,6 +38,9 @@ AutomatedPublishingConfig _$AutomatedPublishingConfigFromJson( gcp: json['gcp'] == null ? null : GcpPublishingConfig.fromJson(json['gcp'] as Map), + manual: json['manual'] == null + ? null + : ManualPublishingConfig.fromJson(json['manual'] as Map), ); Map _$AutomatedPublishingConfigToJson( @@ -45,6 +48,7 @@ Map _$AutomatedPublishingConfigToJson( ) => { 'github': ?instance.github?.toJson(), 'gcp': ?instance.gcp?.toJson(), + 'manual': ?instance.manual?.toJson(), }; GitHubPublishingConfig _$GitHubPublishingConfigFromJson( @@ -85,6 +89,14 @@ Map _$GcpPublishingConfigToJson( 'serviceAccountEmail': ?instance.serviceAccountEmail, }; +ManualPublishingConfig _$ManualPublishingConfigFromJson( + Map json, +) => ManualPublishingConfig(isDisabled: json['isDisabled'] as bool? ?? false); + +Map _$ManualPublishingConfigToJson( + ManualPublishingConfig instance, +) => {'isDisabled': instance.isDisabled}; + VersionOptions _$VersionOptionsFromJson(Map json) => VersionOptions(isRetracted: json['isRetracted'] as bool?); diff --git a/pkg/pub_integration/lib/src/test_browser.dart b/pkg/pub_integration/lib/src/test_browser.dart index c3817f80ce..f19829d230 100644 --- a/pkg/pub_integration/lib/src/test_browser.dart +++ b/pkg/pub_integration/lib/src/test_browser.dart @@ -372,9 +372,9 @@ extension PageExt on Page { } /// Returns the [property] value of the first element by [selector]. - Future propertyValue(String selector, String property) async { + Future propertyValue(String selector, String property) async { final h = await $(selector); - return await h.propertyValue(property); + return await h.propertyValue(property); } } diff --git a/pkg/pub_integration/test/pkg_admin_page_test.dart b/pkg/pub_integration/test/pkg_admin_page_test.dart index 0ed68ac015..2554efa492 100644 --- a/pkg/pub_integration/test/pkg_admin_page_test.dart +++ b/pkg/pub_integration/test/pkg_admin_page_test.dart @@ -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'); diff --git a/pkg/web_app/lib/src/admin_pages.dart b/pkg/web_app/lib/src/admin_pages.dart index a44af4af75..48bab12349 100644 --- a/pkg/web_app/lib/src/admin_pages.dart +++ b/pkg/web_app/lib/src/admin_pages.dart @@ -85,6 +85,7 @@ class _PkgAdminWidget { InputElement? _replacedByInput; Element? _replacedByButton; InputElement? _unlistedCheckbox; + InputElement? _disableManualPublishingCheckbox; Element? _inviteUploaderButton; Element? _inviteUploaderContent; InputElement? _inviteUploaderInput; @@ -112,6 +113,12 @@ class _PkgAdminWidget { _unlistedCheckbox = document.getElementById('-admin-is-unlisted-checkbox') as InputElement?; _unlistedCheckbox?.onChange.listen((_) => _toggleUnlisted()); + _disableManualPublishingCheckbox = + document.getElementById('-admin-is-manual-publishing-disabled') + as InputElement?; + _disableManualPublishingCheckbox?.onChange.listen( + (_) => _toggleManualPublishingDisabled(), + ); _inviteUploaderButton = document.getElementById( '-pkg-admin-invite-uploader-button', ); @@ -338,6 +345,32 @@ class _PkgAdminWidget { } } + Future _toggleManualPublishingDisabled() async { + final oldValue = _disableManualPublishingCheckbox!.defaultChecked ?? false; + final newValue = await api_client.rpc( + confirmQuestion: text( + 'Are you sure you want change the manual publishing status of the package?', + ), + fn: () async { + final rs = await api_client.client.setAutomatedPublishing( + pageData.pkgData!.package, + AutomatedPublishingConfig( + manual: ManualPublishingConfig(isDisabled: !oldValue), + ), + ); + return rs.manual?.isDisabled; + }, + successMessage: text('Manual publishing status changed.'), + onError: (err) => null, + ); + if (newValue == null) { + _disableManualPublishingCheckbox!.checked = oldValue; + } else { + _disableManualPublishingCheckbox!.defaultChecked = newValue; + _disableManualPublishingCheckbox!.checked = newValue; + } + } + Future _setRetracted() async { final version = materialDropdownSelected(_retractPackageVersionInput)?.trim() ?? ''; From 5d00f6a9a524c9b64cd243cb71b31f1c2a519417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Thu, 9 Oct 2025 10:09:14 +0200 Subject: [PATCH 2/9] Update app/lib/shared/exceptions.dart Co-authored-by: Sigurd Meldgaard --- app/lib/shared/exceptions.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/shared/exceptions.dart b/app/lib/shared/exceptions.dart index 56cf83ccff..870d14a584 100644 --- a/app/lib/shared/exceptions.dart +++ b/app/lib/shared/exceptions.dart @@ -575,7 +575,7 @@ class AuthorizationException extends ResponseException { /// 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.', + 'Manual publishing with the `pub` tool has been disabled. This usually means this package should be published via automated publishing (see https://dart.dev/tools/pub/automated-publishing). To re-enable manual publishing, go to the package admin page.', ); @override From 80c63814d58180f4130dc0ac0698d9dbbe86aae0 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Thu, 9 Oct 2025 10:37:12 +0200 Subject: [PATCH 3/9] fixed test message check --- app/test/package/upload_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/test/package/upload_test.dart b/app/test/package/upload_test.dart index 6dc4320c15..253f1e47c9 100644 --- a/app/test/package/upload_test.dart +++ b/app/test/package/upload_test.dart @@ -306,8 +306,7 @@ void main() { rs, status: 403, code: 'InsufficientPermissions', - message: - 'The manual publishing with the `pub` tool is disabled on the package admin page.', + message: 'Manual publishing with the `pub` tool has been disabled.', ); }, ); From 936c11d095e67b50ba603cdd572553ceb05e8caa Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Thu, 9 Oct 2025 13:24:16 +0200 Subject: [PATCH 4/9] Updated field name, admin page and handling. --- .../templates/views/pkg/admin_page.dart | 38 ++++++++++------- app/lib/package/backend.dart | 6 +-- .../package/automated_publishing_test.dart | 14 +++---- app/test/package/upload_test.dart | 2 +- pkg/_pub_shared/lib/data/package_api.dart | 4 +- pkg/_pub_shared/lib/data/package_api.g.dart | 4 +- .../test/pkg_admin_page_test.dart | 12 ++++-- pkg/web_app/lib/src/admin_pages.dart | 41 ++++--------------- 8 files changed, 54 insertions(+), 67 deletions(-) diff --git a/app/lib/frontend/templates/views/pkg/admin_page.dart b/app/lib/frontend/templates/views/pkg/admin_page.dart index 449a04dfe5..58fae1cf9a 100644 --- a/app/lib/frontend/templates/views/pkg/admin_page.dart +++ b/app/lib/frontend/templates/views/pkg/admin_page.dart @@ -33,19 +33,27 @@ d.Node packageAdminPageNode({ ], ), TocNode( - 'Automated publishing', - href: '#automated-publishing', + 'Publishing', + href: '#publishing', children: [ - TocNode('GitHub Actions', href: '#github-actions'), + if (requestContext + .experimentalFlags + .isManualPublishingConfigAvailable) + TocNode('Manual publishing', href: '#manual-publishing'), TocNode( - 'Google Cloud Service account', - href: '#google-cloud-service-account', + 'Automated publishing', + href: '#automated-publishing', + children: [ + TocNode('GitHub Actions', href: '#github-actions'), + TocNode( + 'Google Cloud Service account', + href: '#google-cloud-service-account', + ), + ], ), + TocNode('Version retraction', href: '#version-retraction'), ], ), - if (requestContext.experimentalFlags.isManualPublishingConfigAvailable) - TocNode('Manual publishing', href: '#manual-publishing'), - TocNode('Version retraction', href: '#version-retraction'), ]), d.a(name: 'ownership'), d.h2(text: 'Package ownership'), @@ -229,9 +237,11 @@ d.Node packageAdminPageNode({ ), ), ], - _automatedPublishing(package), + d.a(name: 'publishing'), + d.h2(text: 'Publishing'), if (requestContext.experimentalFlags.isManualPublishingConfigAvailable) _manualPublishing(package), + _automatedPublishing(package), d.a(name: 'version-retraction'), d.h2(text: 'Version retraction'), d.div( @@ -309,7 +319,7 @@ d.Node _automatedPublishing(Package package) { final isGitHubEnabled = github?.isEnabled ?? false; return d.fragment([ d.a(name: 'automated-publishing'), - d.h2(text: 'Automated publishing'), + d.h3(text: 'Automated publishing'), d.markdown( 'You can automate publishing from the supported automated deployment environments. ' 'Instead of creating long-lived secrets, you may use temporary OpenID-Connect tokens ' @@ -462,7 +472,7 @@ d.Node _manualPublishing(Package package) { final manual = package.automatedPublishing?.manualConfig; return d.fragment([ d.a(name: 'manual-publishing'), - d.h2(text: 'Manual publishing'), + d.h3(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 ' @@ -471,9 +481,9 @@ d.Node _manualPublishing(Package package) { d.div( classes: ['-pub-form-checkbox-row'], child: material.checkbox( - id: '-admin-is-manual-publishing-disabled', - label: 'Disable manual publishing', - checked: manual?.isDisabled ?? false, + id: '-pkg-admin-manual-publishing-enabled', + label: 'Enable manual publishing', + checked: manual?.isEnabled ?? true, ), ), ]); diff --git a/app/lib/package/backend.dart b/app/lib/package/backend.dart index b53911ed78..186ca2547e 100644 --- a/app/lib/package/backend.dart +++ b/app/lib/package/backend.dart @@ -1616,9 +1616,9 @@ class PackageBackend { } if (agent is AuthenticatedUser && await packageBackend.isPackageAdmin(package, agent.user.userId)) { - final isDisabled = - package.automatedPublishing?.manualConfig?.isDisabled ?? false; - if (isDisabled) { + final isEnabled = + package.automatedPublishing?.manualConfig?.isEnabled ?? true; + if (!isEnabled) { throw AuthorizationException.manualPublishingDisabled(); } return; diff --git a/app/test/package/automated_publishing_test.dart b/app/test/package/automated_publishing_test.dart index 1f8227649c..69aa9b973d 100644 --- a/app/test/package/automated_publishing_test.dart +++ b/app/test/package/automated_publishing_test.dart @@ -182,7 +182,7 @@ void main() { 'oxygen', AutomatedPublishingConfig( github: GitHubPublishingConfig( - isEnabled: false, + isEnabled: true, repository: 'abcd/efgh', tagPattern: pattern, ), @@ -331,9 +331,9 @@ void main() { } await update( - manual: ManualPublishingConfig(isDisabled: false), + manual: ManualPublishingConfig(isEnabled: true), expected: { - 'manual': {'isDisabled': false}, + 'manual': {'isEnabled': true}, }, ); @@ -346,12 +346,12 @@ void main() { 'isPushEventEnabled': true, 'isWorkflowDispatchEventEnabled': false, }, - 'manual': {'isDisabled': false}, + 'manual': {'isEnabled': true}, }, ); await update( - manual: ManualPublishingConfig(isDisabled: true), + manual: ManualPublishingConfig(isEnabled: false), expected: { 'github': { 'isEnabled': false, @@ -359,7 +359,7 @@ void main() { 'isPushEventEnabled': true, 'isWorkflowDispatchEventEnabled': false, }, - 'manual': {'isDisabled': true}, + 'manual': {'isEnabled': false}, }, ); @@ -378,7 +378,7 @@ void main() { 'isPushEventEnabled': true, 'isWorkflowDispatchEventEnabled': false, }, - 'manual': {'isDisabled': true}, + 'manual': {'isEnabled': false}, }, ); }, diff --git a/app/test/package/upload_test.dart b/app/test/package/upload_test.dart index 253f1e47c9..c01fd0422e 100644 --- a/app/test/package/upload_test.dart +++ b/app/test/package/upload_test.dart @@ -291,7 +291,7 @@ void main() { await client.setAutomatedPublishing( 'oxygen', AutomatedPublishingConfig( - manual: ManualPublishingConfig(isDisabled: true), + manual: ManualPublishingConfig(isEnabled: false), ), ); }); diff --git a/pkg/_pub_shared/lib/data/package_api.dart b/pkg/_pub_shared/lib/data/package_api.dart index 2066e0a7dc..1936cdab5f 100644 --- a/pkg/_pub_shared/lib/data/package_api.dart +++ b/pkg/_pub_shared/lib/data/package_api.dart @@ -123,9 +123,9 @@ class GcpPublishingConfig { @JsonSerializable(includeIfNull: false, explicitToJson: true) class ManualPublishingConfig { - bool isDisabled; + bool isEnabled; - ManualPublishingConfig({this.isDisabled = false}); + ManualPublishingConfig({this.isEnabled = true}); factory ManualPublishingConfig.fromJson(Map json) => _$ManualPublishingConfigFromJson(json); diff --git a/pkg/_pub_shared/lib/data/package_api.g.dart b/pkg/_pub_shared/lib/data/package_api.g.dart index 1ca91ffb7e..9dac845b04 100644 --- a/pkg/_pub_shared/lib/data/package_api.g.dart +++ b/pkg/_pub_shared/lib/data/package_api.g.dart @@ -91,11 +91,11 @@ Map _$GcpPublishingConfigToJson( ManualPublishingConfig _$ManualPublishingConfigFromJson( Map json, -) => ManualPublishingConfig(isDisabled: json['isDisabled'] as bool? ?? false); +) => ManualPublishingConfig(isEnabled: json['isEnabled'] as bool? ?? true); Map _$ManualPublishingConfigToJson( ManualPublishingConfig instance, -) => {'isDisabled': instance.isDisabled}; +) => {'isEnabled': instance.isEnabled}; VersionOptions _$VersionOptionsFromJson(Map json) => VersionOptions(isRetracted: json['isRetracted'] as bool?); diff --git a/pkg/pub_integration/test/pkg_admin_page_test.dart b/pkg/pub_integration/test/pkg_admin_page_test.dart index 2554efa492..0f7ca1af4e 100644 --- a/pkg/pub_integration/test/pkg_admin_page_test.dart +++ b/pkg/pub_integration/test/pkg_admin_page_test.dart @@ -84,15 +84,19 @@ void main() { 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.waitAndClick('#-pkg-admin-manual-publishing-enabled'); + await page.waitAndClick( + '#-pkg-admin-automated-button', + waitForOneResponse: true, + ); + await page.waitAndClickOnDialogOk(); await page.reload(); final value = await page.propertyValue( - '#-admin-is-manual-publishing-disabled', + '#-pkg-admin-manual-publishing-enabled', 'checked', ); - expect(value, true); + expect(value, false); }); // visit activity log page diff --git a/pkg/web_app/lib/src/admin_pages.dart b/pkg/web_app/lib/src/admin_pages.dart index 48bab12349..68eab94aa2 100644 --- a/pkg/web_app/lib/src/admin_pages.dart +++ b/pkg/web_app/lib/src/admin_pages.dart @@ -85,7 +85,6 @@ class _PkgAdminWidget { InputElement? _replacedByInput; Element? _replacedByButton; InputElement? _unlistedCheckbox; - InputElement? _disableManualPublishingCheckbox; Element? _inviteUploaderButton; Element? _inviteUploaderContent; InputElement? _inviteUploaderInput; @@ -113,12 +112,6 @@ class _PkgAdminWidget { _unlistedCheckbox = document.getElementById('-admin-is-unlisted-checkbox') as InputElement?; _unlistedCheckbox?.onChange.listen((_) => _toggleUnlisted()); - _disableManualPublishingCheckbox = - document.getElementById('-admin-is-manual-publishing-disabled') - as InputElement?; - _disableManualPublishingCheckbox?.onChange.listen( - (_) => _toggleManualPublishingDisabled(), - ); _inviteUploaderButton = document.getElementById( '-pkg-admin-invite-uploader-button', ); @@ -156,6 +149,9 @@ class _PkgAdminWidget { } void _setupAutomatedPublishing() { + final manualPublishingEnabledCheckbox = + document.getElementById('-pkg-admin-manual-publishing-enabled') + as InputElement?; final githubEnabledCheckbox = document.getElementById('-pkg-admin-automated-github-enabled') as InputElement?; @@ -194,7 +190,7 @@ class _PkgAdminWidget { updateButton.onClick.listen((event) async { await api_client.rpc( confirmQuestion: await markdown( - 'Are you sure you want to update the automated publishing config?', + 'Are you sure you want to update the publishing config?', ), fn: () async { await api_client.client.setAutomatedPublishing( @@ -216,6 +212,9 @@ class _PkgAdminWidget { isEnabled: gcpEnabledCheckbox!.checked ?? false, serviceAccountEmail: gcpServiceAccountEmailInput!.value, ), + manual: ManualPublishingConfig( + isEnabled: manualPublishingEnabledCheckbox?.checked ?? true, + ), ), ); }, @@ -345,32 +344,6 @@ class _PkgAdminWidget { } } - Future _toggleManualPublishingDisabled() async { - final oldValue = _disableManualPublishingCheckbox!.defaultChecked ?? false; - final newValue = await api_client.rpc( - confirmQuestion: text( - 'Are you sure you want change the manual publishing status of the package?', - ), - fn: () async { - final rs = await api_client.client.setAutomatedPublishing( - pageData.pkgData!.package, - AutomatedPublishingConfig( - manual: ManualPublishingConfig(isDisabled: !oldValue), - ), - ); - return rs.manual?.isDisabled; - }, - successMessage: text('Manual publishing status changed.'), - onError: (err) => null, - ); - if (newValue == null) { - _disableManualPublishingCheckbox!.checked = oldValue; - } else { - _disableManualPublishingCheckbox!.defaultChecked = newValue; - _disableManualPublishingCheckbox!.checked = newValue; - } - } - Future _setRetracted() async { final version = materialDropdownSelected(_retractPackageVersionInput)?.trim() ?? ''; From fe730880ed9a902ecc01dc3fdcd12838c02aaefc Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Thu, 9 Oct 2025 13:39:41 +0200 Subject: [PATCH 5/9] test golden files --- app/test/frontend/golden/pkg_admin_page.html | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/test/frontend/golden/pkg_admin_page.html b/app/test/frontend/golden/pkg_admin_page.html index 042e02fde1..d8cac0068b 100644 --- a/app/test/frontend/golden/pkg_admin_page.html +++ b/app/test/frontend/golden/pkg_admin_page.html @@ -262,15 +262,18 @@

Metadata

Unlisted + -
+ - @@ -404,8 +407,10 @@

Unlisted

+ +

Publishing

-

Automated publishing

+

Automated publishing

You can automate publishing from the supported automated deployment environments. Instead of creating long-lived secrets, you may use temporary OpenID-Connect tokens signed by either GitHub Actions or Google Cloud IAM. See the pub automated publishing guide From db8b0a5202b649771c89f898f11d975f09fa190f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Thu, 9 Oct 2025 18:08:35 +0200 Subject: [PATCH 6/9] Update app/lib/shared/exceptions.dart Co-authored-by: Sigurd Meldgaard --- app/lib/shared/exceptions.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/shared/exceptions.dart b/app/lib/shared/exceptions.dart index 870d14a584..6e512b507d 100644 --- a/app/lib/shared/exceptions.dart +++ b/app/lib/shared/exceptions.dart @@ -575,7 +575,7 @@ class AuthorizationException extends ResponseException { /// Signaling that the manual publishing was disabled and cannot be authorized. factory AuthorizationException.manualPublishingDisabled() => AuthorizationException._( - 'Manual publishing with the `pub` tool has been disabled. This usually means this package should be published via automated publishing (see https://dart.dev/tools/pub/automated-publishing). To re-enable manual publishing, go to the package admin page.', + 'Manual publishing has been disabled. This usually means this package should be published via automated publishing (see https://dart.dev/tools/pub/automated-publishing). To re-enable manual publishing, go to the package admin page.', ); @override From b129f4e0f69f1b75736b7577521e2dc300f520f8 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Thu, 9 Oct 2025 18:36:24 +0200 Subject: [PATCH 7/9] test fix --- app/test/package/upload_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/test/package/upload_test.dart b/app/test/package/upload_test.dart index c01fd0422e..dbf1ec923f 100644 --- a/app/test/package/upload_test.dart +++ b/app/test/package/upload_test.dart @@ -306,7 +306,7 @@ void main() { rs, status: 403, code: 'InsufficientPermissions', - message: 'Manual publishing with the `pub` tool has been disabled.', + message: 'Manual publishing has been disabled.', ); }, ); From 5a897d5b69d31b926ef2f14b1c2a02d914cae786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Fri, 10 Oct 2025 12:00:21 +0200 Subject: [PATCH 8/9] Update app/lib/frontend/templates/views/pkg/admin_page.dart Co-authored-by: Sigurd Meldgaard --- app/lib/frontend/templates/views/pkg/admin_page.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/lib/frontend/templates/views/pkg/admin_page.dart b/app/lib/frontend/templates/views/pkg/admin_page.dart index 58fae1cf9a..a8788118b9 100644 --- a/app/lib/frontend/templates/views/pkg/admin_page.dart +++ b/app/lib/frontend/templates/views/pkg/admin_page.dart @@ -474,9 +474,12 @@ d.Node _manualPublishing(Package package) { d.a(name: 'manual-publishing'), d.h3(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.', + ''' +Manual publishing, using personal credentials for the `pub` client (`pub login`) . + +Disable to prevent accidental publication from the command line. + +It is recommended to disable when automated publishing is enabled.''', ), d.div( classes: ['-pub-form-checkbox-row'], From 0f1443d5eafe4e6cce1509e125dcb367f4bfb5b5 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Fri, 10 Oct 2025 12:35:53 +0200 Subject: [PATCH 9/9] admin page url in the exception message --- .../frontend/templates/views/pkg/admin_page.dart | 6 ++---- app/lib/package/backend.dart | 2 +- app/lib/shared/exceptions.dart | 14 ++++++++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/lib/frontend/templates/views/pkg/admin_page.dart b/app/lib/frontend/templates/views/pkg/admin_page.dart index a8788118b9..5eb1324272 100644 --- a/app/lib/frontend/templates/views/pkg/admin_page.dart +++ b/app/lib/frontend/templates/views/pkg/admin_page.dart @@ -473,14 +473,12 @@ d.Node _manualPublishing(Package package) { return d.fragment([ d.a(name: 'manual-publishing'), d.h3(text: 'Manual publishing'), - d.markdown( - ''' + d.markdown(''' Manual publishing, using personal credentials for the `pub` client (`pub login`) . Disable to prevent accidental publication from the command line. -It is recommended to disable when automated publishing is enabled.''', - ), +It is recommended to disable when automated publishing is enabled.'''), d.div( classes: ['-pub-form-checkbox-row'], child: material.checkbox( diff --git a/app/lib/package/backend.dart b/app/lib/package/backend.dart index 186ca2547e..6943c0f897 100644 --- a/app/lib/package/backend.dart +++ b/app/lib/package/backend.dart @@ -1619,7 +1619,7 @@ class PackageBackend { final isEnabled = package.automatedPublishing?.manualConfig?.isEnabled ?? true; if (!isEnabled) { - throw AuthorizationException.manualPublishingDisabled(); + throw AuthorizationException.manualPublishingDisabled(package.name!); } return; } diff --git a/app/lib/shared/exceptions.dart b/app/lib/shared/exceptions.dart index 6e512b507d..658c4235d8 100644 --- a/app/lib/shared/exceptions.dart +++ b/app/lib/shared/exceptions.dart @@ -16,6 +16,7 @@ library; import 'dart:io'; import 'package:api_builder/api_builder.dart' show ApiResponseException; +import 'package:pub_dev/shared/urls.dart'; import 'package:pub_dev/shared/utils.dart'; /// Base class for all exceptions that are intercepted by HTTP handler wrappers. @@ -573,10 +574,15 @@ class AuthorizationException extends ResponseException { ); /// Signaling that the manual publishing was disabled and cannot be authorized. - factory AuthorizationException.manualPublishingDisabled() => - AuthorizationException._( - 'Manual publishing has been disabled. This usually means this package should be published via automated publishing (see https://dart.dev/tools/pub/automated-publishing). To re-enable manual publishing, go to the package admin page.', - ); + factory AuthorizationException.manualPublishingDisabled(String package) { + return AuthorizationException._( + 'Manual publishing has been disabled. ' + 'This usually means this package should be published via automated publishing ' + '(see https://dart.dev/tools/pub/automated-publishing). ' + 'To re-enable manual publishing, go to the package admin page ' + '(see ${pkgAdminUrl(package, includeHost: true)}).', + ); + } @override String toString() => '$code: $message'; // used by package:pub_server