Skip to content

Commit c86eae0

Browse files
committed
Option to disable manual publishing
1 parent 412b3f2 commit c86eae0

File tree

13 files changed

+245
-7
lines changed

13 files changed

+245
-7
lines changed

app/lib/frontend/handlers/experimental.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const _publicFlags = <PublicFlag>{
1414

1515
final _allFlags = <String>{
1616
'dark-as-default',
17+
'manual-publishing',
1718
..._publicFlags.map((x) => x.name),
1819
};
1920

@@ -88,6 +89,8 @@ class ExperimentalFlags {
8889

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

92+
bool get isManualPublishingConfigAvailable => isEnabled('manual-publishing');
93+
9194
String encodedAsCookie() => _enabled.join(':');
9295

9396
@override

app/lib/frontend/templates/views/pkg/admin_page.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'package:_pub_shared/data/package_api.dart';
6+
import 'package:pub_dev/frontend/request_context.dart';
67

78
import '../../../../account/models.dart';
89
import '../../../../package/models.dart';
@@ -42,6 +43,8 @@ d.Node packageAdminPageNode({
4243
),
4344
],
4445
),
46+
if (requestContext.experimentalFlags.isManualPublishingConfigAvailable)
47+
TocNode('Manual publishing', href: '#manual-publishing'),
4548
TocNode('Version retraction', href: '#version-retraction'),
4649
]),
4750
d.a(name: 'ownership'),
@@ -227,6 +230,8 @@ d.Node packageAdminPageNode({
227230
),
228231
],
229232
_automatedPublishing(package),
233+
if (requestContext.experimentalFlags.isManualPublishingConfigAvailable)
234+
_manualPublishing(package),
230235
d.a(name: 'version-retraction'),
231236
d.h2(text: 'Version retraction'),
232237
d.div(
@@ -453,6 +458,27 @@ d.Node _automatedPublishing(Package package) {
453458
]);
454459
}
455460

461+
d.Node _manualPublishing(Package package) {
462+
final manual = package.automatedPublishing?.manualConfig;
463+
return d.fragment([
464+
d.a(name: 'manual-publishing'),
465+
d.h2(text: 'Manual publishing'),
466+
d.markdown(
467+
'The manual publishing of new versions using the `pub` tool is enabled by default in all packages. '
468+
'Disabling it may protect the package from accidental publishing events when the package is otherwise using '
469+
'automated publishing, or in other cases, is discontinued.',
470+
),
471+
d.div(
472+
classes: ['-pub-form-checkbox-row'],
473+
child: material.checkbox(
474+
id: '-admin-is-manual-publishing-disabled',
475+
label: 'Disable manual publishing',
476+
checked: manual?.isDisabled ?? false,
477+
),
478+
),
479+
]);
480+
}
481+
456482
d.Node _exampleGitHubWorkflow(GitHubPublishingConfig github) {
457483
final expandedTagPattern = (github.tagPattern ?? '{{version}}').replaceAll(
458484
'{{version}}',

app/lib/package/backend.dart

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,8 @@ class PackageBackend {
635635
final p = await tx.lookupValue<Package>(pkg.key);
636636
final githubConfig = body.github;
637637
final gcpConfig = body.gcp;
638+
final manualConfig = body.manual;
639+
638640
if (githubConfig != null) {
639641
final isEnabled = githubConfig.isEnabled;
640642

@@ -648,7 +650,9 @@ class PackageBackend {
648650
final repository = githubConfig.repository?.trim() ?? '';
649651
githubConfig.repository = repository.isEmpty ? null : repository;
650652
final tagPattern = githubConfig.tagPattern?.trim() ?? '';
651-
verifyTagPattern(tagPattern: tagPattern);
653+
if (isEnabled) {
654+
verifyTagPattern(tagPattern: tagPattern);
655+
}
652656
githubConfig.tagPattern = tagPattern.isEmpty ? null : tagPattern;
653657
final environment = githubConfig.environment?.trim() ?? '';
654658
githubConfig.environment = environment.isEmpty ? null : environment;
@@ -726,9 +730,14 @@ class PackageBackend {
726730
}
727731

728732
// finalize changes
729-
p.automatedPublishing ??= AutomatedPublishing();
730-
p.automatedPublishing!.githubConfig = githubConfig;
731-
p.automatedPublishing!.gcpConfig = gcpConfig;
733+
final automatedPublishing = p.automatedPublishing ??=
734+
AutomatedPublishing();
735+
automatedPublishing.githubConfig =
736+
githubConfig ?? automatedPublishing.githubConfig;
737+
automatedPublishing.gcpConfig =
738+
gcpConfig ?? automatedPublishing.gcpConfig;
739+
automatedPublishing.manualConfig =
740+
manualConfig ?? automatedPublishing.manualConfig;
732741

733742
p.updated = clock.now().toUtc();
734743
tx.insert(p);
@@ -742,6 +751,7 @@ class PackageBackend {
742751
return api.AutomatedPublishingConfig(
743752
github: p.automatedPublishing!.githubConfig,
744753
gcp: p.automatedPublishing!.gcpConfig,
754+
manual: p.automatedPublishing!.manualConfig,
745755
);
746756
});
747757
}
@@ -1606,6 +1616,11 @@ class PackageBackend {
16061616
}
16071617
if (agent is AuthenticatedUser &&
16081618
await packageBackend.isPackageAdmin(package, agent.user.userId)) {
1619+
final isDisabled =
1620+
package.automatedPublishing?.manualConfig?.isDisabled ?? false;
1621+
if (isDisabled) {
1622+
throw AuthorizationException.manualPublishingDisabled();
1623+
}
16091624
return;
16101625
}
16111626
if (agent is AuthenticatedGitHubAction) {

app/lib/package/models.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,12 +460,14 @@ class AutomatedPublishing {
460460
GitHubPublishingLock? githubLock;
461461
GcpPublishingConfig? gcpConfig;
462462
GcpPublishingLock? gcpLock;
463+
ManualPublishingConfig? manualConfig;
463464

464465
AutomatedPublishing({
465466
this.githubConfig,
466467
this.githubLock,
467468
this.gcpConfig,
468469
this.gcpLock,
470+
this.manualConfig,
469471
});
470472

471473
factory AutomatedPublishing.fromJson(Map<String, dynamic> json) =>

app/lib/package/models.g.dart

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/lib/shared/exceptions.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,12 @@ class AuthorizationException extends ResponseException {
572572
'The calling service account is not allowed to publish, because: $reason.\nSee https://dart.dev/go/publishing-with-service-account',
573573
);
574574

575+
/// Signaling that the manual publishing was disabled and cannot be authorized.
576+
factory AuthorizationException.manualPublishingDisabled() =>
577+
AuthorizationException._(
578+
'The manual publishing with the `pub` tool is disabled on the package admin page.',
579+
);
580+
575581
@override
576582
String toString() => '$code: $message'; // used by package:pub_server
577583
}

app/test/package/automated_publishing_test.dart

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,5 +309,79 @@ void main() {
309309
);
310310
},
311311
);
312+
313+
testWithProfile(
314+
'partial settings do not override the other',
315+
fn: () async {
316+
final client = await createFakeAuthPubApiClient(
317+
email: adminAtPubDevEmail,
318+
);
319+
320+
Future<void> update({
321+
GitHubPublishingConfig? github,
322+
GcpPublishingConfig? gcp,
323+
ManualPublishingConfig? manual,
324+
required Map<String, dynamic> expected,
325+
}) async {
326+
final rs = await client.setAutomatedPublishing(
327+
'oxygen',
328+
AutomatedPublishingConfig(github: github, gcp: gcp, manual: manual),
329+
);
330+
expect(rs.toJson(), expected);
331+
}
332+
333+
await update(
334+
manual: ManualPublishingConfig(isDisabled: false),
335+
expected: {
336+
'manual': {'isDisabled': false},
337+
},
338+
);
339+
340+
await update(
341+
github: GitHubPublishingConfig(isEnabled: false),
342+
expected: {
343+
'github': {
344+
'isEnabled': false,
345+
'requireEnvironment': false,
346+
'isPushEventEnabled': true,
347+
'isWorkflowDispatchEventEnabled': false,
348+
},
349+
'manual': {'isDisabled': false},
350+
},
351+
);
352+
353+
await update(
354+
manual: ManualPublishingConfig(isDisabled: true),
355+
expected: {
356+
'github': {
357+
'isEnabled': false,
358+
'requireEnvironment': false,
359+
'isPushEventEnabled': true,
360+
'isWorkflowDispatchEventEnabled': false,
361+
},
362+
'manual': {'isDisabled': true},
363+
},
364+
);
365+
366+
await update(
367+
github: GitHubPublishingConfig(
368+
isEnabled: true,
369+
tagPattern: '{{version}}',
370+
repository: 'user/repo',
371+
),
372+
expected: {
373+
'github': {
374+
'isEnabled': true,
375+
'repository': 'user/repo',
376+
'tagPattern': '{{version}}',
377+
'requireEnvironment': false,
378+
'isPushEventEnabled': true,
379+
'isWorkflowDispatchEventEnabled': false,
380+
},
381+
'manual': {'isDisabled': true},
382+
},
383+
);
384+
},
385+
);
312386
});
313387
}

app/test/package/upload_test.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,38 @@ void main() {
281281
);
282282
});
283283

284+
group('Manual publishing overrides', () {
285+
testWithProfile(
286+
'manual publishing disabled',
287+
fn: () async {
288+
await withFakeAuthRetryPubApiClient(email: adminAtPubDevEmail, (
289+
client,
290+
) async {
291+
await client.setAutomatedPublishing(
292+
'oxygen',
293+
AutomatedPublishingConfig(
294+
manual: ManualPublishingConfig(isDisabled: true),
295+
),
296+
);
297+
});
298+
299+
final bytes = await packageArchiveBytes(
300+
pubspecContent: generatePubspecYaml('oxygen', '2.2.0'),
301+
);
302+
final rs = createPubApiClient(
303+
authToken: adminClientToken,
304+
).uploadPackageBytes(bytes);
305+
await expectApiException(
306+
rs,
307+
status: 403,
308+
code: 'InsufficientPermissions',
309+
message:
310+
'The manual publishing with the `pub` tool is disabled on the package admin page.',
311+
);
312+
},
313+
);
314+
});
315+
284316
group('Uploading with service account', () {
285317
testWithProfile(
286318
'service account cannot upload new package',

pkg/_pub_shared/lib/data/package_api.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ class PkgOptions {
4646
class AutomatedPublishingConfig {
4747
final GitHubPublishingConfig? github;
4848
final GcpPublishingConfig? gcp;
49+
final ManualPublishingConfig? manual;
4950

50-
AutomatedPublishingConfig({this.github, this.gcp});
51+
AutomatedPublishingConfig({this.github, this.gcp, this.manual});
5152

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

124+
@JsonSerializable(includeIfNull: false, explicitToJson: true)
125+
class ManualPublishingConfig {
126+
bool isDisabled;
127+
128+
ManualPublishingConfig({this.isDisabled = false});
129+
130+
factory ManualPublishingConfig.fromJson(Map<String, dynamic> json) =>
131+
_$ManualPublishingConfigFromJson(json);
132+
133+
Map<String, dynamic> toJson() => _$ManualPublishingConfigToJson(this);
134+
}
135+
123136
@JsonSerializable()
124137
class VersionOptions {
125138
final bool? isRetracted;

pkg/_pub_shared/lib/data/package_api.g.dart

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)