From fd1d3cb0bd5d93abeb1d24c0f21cf045122f0987 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Fri, 5 Jun 2026 00:35:33 +0200 Subject: [PATCH 1/5] Test preview deployment instead of comment --- .../lib/src/commands/stage_preview.dart | 272 ++++++++++++++---- 1 file changed, 219 insertions(+), 53 deletions(-) diff --git a/tool/dash_site/lib/src/commands/stage_preview.dart b/tool/dash_site/lib/src/commands/stage_preview.dart index 0e7f8a124d..ea9a8fbd37 100644 --- a/tool/dash_site/lib/src/commands/stage_preview.dart +++ b/tool/dash_site/lib/src/commands/stage_preview.dart @@ -16,7 +16,7 @@ import 'build.dart'; /// Builds the selected or default site, /// deploys it to a Firebase Hosting preview channel, -/// and posts or updates a comment on the source GitHub PR when requested. +/// and publishes a deployment status on the source GitHub PR when requested. final class StagePreviewCommand extends Command { static const String _projectOption = 'project'; static const String _channelOption = 'channel'; @@ -52,22 +52,22 @@ final class StagePreviewCommand extends Command { ..addOption( _prNumberOption, help: - 'The pull request number to comment on. ' - 'Required together with --$_repoOption to post a preview comment.', + 'The pull request number to publish a deployment for. ' + 'Required together with --$_repoOption to publish a deployment.', ) ..addOption( _repoOption, help: 'The full repository name in "owner/repository" form. ' 'Required together with --$_prNumberOption to ' - 'post a preview comment.', + 'publish a deployment.', valueHelp: 'owner/repository', ) ..addOption( _commitShaOption, help: 'The commit SHA being staged. ' - 'Required to post a preview comment.', + 'Required to publish a deployment.', ) ..addOption( _headBranchOption, @@ -78,7 +78,7 @@ final class StagePreviewCommand extends Command { @override String get description => 'Build the site, deploy it to a Firebase staging channel, ' - 'and comment the preview URL on GitHub.'; + 'and publish the preview URL as a GitHub deployment.'; @override String get name => 'stage-preview'; @@ -116,7 +116,7 @@ final class StagePreviewCommand extends Command { if (commitSha == null) { stderr.writeln( 'Error: --$_commitShaOption must be set to ' - 'comment on the pull request.', + 'publish a deployment.', ); return 1; } @@ -125,7 +125,7 @@ final class StagePreviewCommand extends Command { if (githubToken == null) { stderr.writeln( 'Error: $githubPatTokenEnv must be set to ' - 'comment on the pull request.', + 'publish a deployment.', ); return 1; } @@ -139,7 +139,7 @@ final class StagePreviewCommand extends Command { if (prNumberArg != null || repoFullName != null) { stderr.writeln( 'Warning: Both --$_prNumberOption and --$_repoOption must be set ' - 'to comment on the pull request; skipping the GitHub comment.', + 'to publish a deployment; skipping the GitHub deployment.', ); } prContext = null; @@ -174,12 +174,12 @@ final class StagePreviewCommand extends Command { } if (prContext == null) { - print('No pull request context available; skipping GitHub comment.'); + print('No pull request context available; skipping GitHub deployment.'); print(stagingUrl); return 0; } - return _commentStagingUrlOnGitHub( + return _publishStagingDeploymentOnGitHub( site: selectedSite, stagingUrl: stagingUrl, context: prContext, @@ -255,52 +255,71 @@ String? _extractDeployedUrl(String firebaseJsonOutput) { return null; } -/// Posts a new preview comment on the pull request, -/// or updates the existing one identified by an HTML marker that -/// includes the [site]'s name. +const String _githubApiVersion = '2026-03-10'; +const String _githubJsonMimeType = 'application/vnd.github+json'; +const String _previewDeploymentTask = 'deploy:preview'; + +/// Publishes [stagingUrl] as a GitHub deployment status. /// /// Returns `0` on success or `1` if the GitHub API call fails. -Future _commentStagingUrlOnGitHub({ +Future _publishStagingDeploymentOnGitHub({ required Site site, required String stagingUrl, required _PullRequestContext context, }) async { - final commentMarker = ''; - final commentBody = - ''' -$commentMarker -Staged preview of the updated ${site.host} site (updated for commit ${context.commitSha}): - -$stagingUrl'''; + final environment = _previewEnvironmentForSite( + site, + prNumber: context.prNumber, + ); - print('Commenting ${site.host} staging URL on the PR...'); + print( + 'Publishing ${site.host} staging URL to GitHub deployment ' + '$environment...', + ); final gitHub = github.GitHub( auth: github.Authentication.withToken(context.githubToken), + version: _githubApiVersion, ); try { final repository = github.RepositorySlug.full(context.repoFullName); - final existingCommentId = await _findExistingPreviewCommentId( + final deploymentId = await _createPreviewDeployment( gitHub: gitHub, repository: repository, - issueNumber: context.prNumber, - commentMarker: commentMarker, + site: site, + stagingUrl: stagingUrl, + environment: environment, + context: context, ); - - if (existingCommentId == null) { - await gitHub.issues.createComment( - repository, - context.prNumber, - commentBody, - ); - } else { - await gitHub.issues.updateComment( - repository, - existingCommentId, - commentBody, + if (deploymentId == null) { + stderr.writeln( + 'Error: Failed to find a deployment id in the GitHub response.', ); + return 1; } + + await _createPreviewDeploymentStatus( + gitHub: gitHub, + repository: repository, + deploymentId: deploymentId, + site: site, + stagingUrl: stagingUrl, + environment: environment, + commitSha: context.commitSha, + ); + + final deploymentIds = await _findPreviewDeploymentIds( + gitHub: gitHub, + repository: repository, + environment: environment, + ); + await _deletePreviewDeployments( + gitHub: gitHub, + repository: repository, + deploymentIds: deploymentIds.where((id) => id < deploymentId).toList(), + environment: environment, + ); } on github.GitHubError catch (error) { - stderr.writeln('Error: Failed to comment on the pull request: $error'); + stderr.writeln('Error: Failed to publish the GitHub deployment: $error'); return 1; } finally { gitHub.dispose(); @@ -309,27 +328,157 @@ $stagingUrl'''; return 0; } -/// Returns the id of the first comment on the issue whose body -/// contains the specified [commentMarker]. -/// -/// Returns `null` if no matching comment exists. -Future _findExistingPreviewCommentId({ +/// Returns existing deployment ids for a PR/site preview [environment]. +Future> _findPreviewDeploymentIds({ required github.GitHub gitHub, required github.RepositorySlug repository, - required int issueNumber, - required String commentMarker, + required String environment, }) async { - await for (final comment in gitHub.issues.listCommentsByIssue( - repository, - issueNumber, - )) { - if (comment.body?.contains(commentMarker) ?? false) { - return comment.id; + final deploymentIds = []; + var page = 1; + while (true) { + final deployments = await gitHub.getJSON>( + '/repos/${repository.fullName}/deployments', + params: { + 'environment': environment, + 'task': _previewDeploymentTask, + 'per_page': '100', + 'page': '$page', + }, + convert: (json) => json as List, + ); + if (deployments.isEmpty) { + return deploymentIds; + } + for (final deployment in deployments) { + if (deployment case {'id': final num id}) { + deploymentIds.add(id.toInt()); + } } + page += 1; + } +} + +/// Creates the GitHub deployment record for the staged preview. +Future _createPreviewDeployment({ + required github.GitHub gitHub, + required github.RepositorySlug repository, + required Site site, + required String stagingUrl, + required String environment, + required _PullRequestContext context, +}) async { + final response = await gitHub.postJSON>( + '/repos/${repository.fullName}/deployments', + statusCode: 201, + body: jsonEncode({ + 'ref': context.commitSha, + 'task': _previewDeploymentTask, + 'auto_merge': false, + 'required_contexts': [], + 'payload': { + 'id': _previewDeploymentPayloadId(site, prNumber: context.prNumber), + 'site': site.name, + 'site_host': site.host, + 'pull_request': context.prNumber, + 'preview_url': stagingUrl, + }, + 'environment': environment, + 'description': 'Staged preview for ${site.host} PR #${context.prNumber}.', + 'transient_environment': true, + 'production_environment': false, + }), + convert: _jsonMap, + ); + if (response case {'id': final num id}) { + return id.toInt(); } return null; } +/// Creates a successful deployment status with [stagingUrl] as the target. +Future _createPreviewDeploymentStatus({ + required github.GitHub gitHub, + required github.RepositorySlug repository, + required int deploymentId, + required Site site, + required String stagingUrl, + required String environment, + required String commitSha, +}) async { + await gitHub.postJSON>( + '/repos/${repository.fullName}/deployments/$deploymentId/statuses', + statusCode: 201, + body: jsonEncode({ + 'state': 'success', + 'log_url': stagingUrl, + 'description': 'Staged ${site.host} preview for ${_shortSha(commitSha)}.', + 'environment': environment, + 'environment_url': stagingUrl, + 'auto_inactive': false, + }), + convert: _jsonMap, + ); +} + +/// Marks and deletes deployments superseded by the newly published preview. +Future _deletePreviewDeployments({ + required github.GitHub gitHub, + required github.RepositorySlug repository, + required List deploymentIds, + required String environment, +}) async { + if (deploymentIds.isEmpty) { + return; + } + + print( + 'Deleting ${deploymentIds.length} older GitHub deployment(s) ' + 'for $environment...', + ); + for (final deploymentId in deploymentIds) { + await _createInactiveDeploymentStatus( + gitHub: gitHub, + repository: repository, + deploymentId: deploymentId, + environment: environment, + ); + await gitHub.request( + 'DELETE', + '/repos/${repository.fullName}/deployments/$deploymentId', + statusCode: 204, + headers: _githubJsonHeaders, + ); + } +} + +/// Marks [deploymentId] inactive so GitHub allows it to be deleted. +Future _createInactiveDeploymentStatus({ + required github.GitHub gitHub, + required github.RepositorySlug repository, + required int deploymentId, + required String environment, +}) async { + await gitHub.postJSON>( + '/repos/${repository.fullName}/deployments/$deploymentId/statuses', + statusCode: 201, + body: jsonEncode({ + 'state': 'inactive', + 'description': 'Superseded by a newer staged preview.', + 'environment': environment, + 'auto_inactive': false, + }), + convert: _jsonMap, + ); +} + +Map get _githubJsonHeaders => { + 'Accept': _githubJsonMimeType, + 'X-GitHub-Api-Version': _githubApiVersion, +}; + +Map _jsonMap(Object? json) => json as Map; + /// Builds a Firebase Hosting channel name for [site] that /// incorporates [branchOrSha] and if specified, [prNumber], /// and satisfies Firebase's naming constraints: @@ -353,13 +502,30 @@ String _firebaseChannelForSite( return channel.replaceAll(RegExp(r'-+$'), ''); } +/// Returns the stable GitHub environment name for a PR/site preview. +/// +/// GitHub assigns deployment ids, so the environment is the stable PR/site key. +String _previewEnvironmentForSite(Site site, {required int prNumber}) => + 'preview-${site.name}-pr-$prNumber'; + +/// Returns an opaque stable payload id for a PR/site preview deployment. +String _previewDeploymentPayloadId(Site site, {required int prNumber}) => + '${site.name}-pr-$prNumber'; + +String _shortSha(String sha) { + if (sha.length <= 7) { + return sha; + } + return sha.substring(0, 7); +} + /// Returns [value] if it is non-null and non-empty, otherwise `null`. String? _nonEmpty(String? value) { if (value == null || value.isEmpty) return null; return value; } -/// Everything required to post a preview comment on a GitHub pull request. +/// Everything required to publish a preview deployment for a pull request. /// /// Built during option parsing so that a misconfigured Cloud Build trigger /// fails before the build and deploy runs. From 71dec32a498374a478df3507efd497c8ca4adf31 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Fri, 5 Jun 2026 01:10:47 +0200 Subject: [PATCH 2/5] Use a cleaner name for preview deployments --- .../lib/src/commands/stage_preview.dart | 104 ++++++++++-------- 1 file changed, 58 insertions(+), 46 deletions(-) diff --git a/tool/dash_site/lib/src/commands/stage_preview.dart b/tool/dash_site/lib/src/commands/stage_preview.dart index ea9a8fbd37..03971a9c57 100644 --- a/tool/dash_site/lib/src/commands/stage_preview.dart +++ b/tool/dash_site/lib/src/commands/stage_preview.dart @@ -255,11 +255,17 @@ String? _extractDeployedUrl(String firebaseJsonOutput) { return null; } +/// The GitHub REST API version used for preview deployment calls. const String _githubApiVersion = '2026-03-10'; -const String _githubJsonMimeType = 'application/vnd.github+json'; + +/// The deployment task used to distinguish site previews from other deploys. const String _previewDeploymentTask = 'deploy:preview'; -/// Publishes [stagingUrl] as a GitHub deployment status. +/// Publishes [stagingUrl] as the latest PR/site preview deployment. +/// +/// Each staging run creates a deployment for the staged commit. The success +/// status is followed by inactive statuses on older successful deployments for +/// the same environment. /// /// Returns `0` on success or `1` if the GitHub API call fails. Future _publishStagingDeploymentOnGitHub({ @@ -282,6 +288,11 @@ Future _publishStagingDeploymentOnGitHub({ ); try { final repository = github.RepositorySlug.full(context.repoFullName); + final previousDeploymentIds = await _findPreviewDeploymentIds( + gitHub: gitHub, + repository: repository, + environment: environment, + ); final deploymentId = await _createPreviewDeployment( gitHub: gitHub, repository: repository, @@ -290,12 +301,6 @@ Future _publishStagingDeploymentOnGitHub({ environment: environment, context: context, ); - if (deploymentId == null) { - stderr.writeln( - 'Error: Failed to find a deployment id in the GitHub response.', - ); - return 1; - } await _createPreviewDeploymentStatus( gitHub: gitHub, @@ -306,16 +311,10 @@ Future _publishStagingDeploymentOnGitHub({ environment: environment, commitSha: context.commitSha, ); - - final deploymentIds = await _findPreviewDeploymentIds( - gitHub: gitHub, - repository: repository, - environment: environment, - ); - await _deletePreviewDeployments( + await _markPreviewDeploymentsInactive( gitHub: gitHub, repository: repository, - deploymentIds: deploymentIds.where((id) => id < deploymentId).toList(), + deploymentIds: previousDeploymentIds, environment: environment, ); } on github.GitHubError catch (error) { @@ -360,7 +359,7 @@ Future> _findPreviewDeploymentIds({ } /// Creates the GitHub deployment record for the staged preview. -Future _createPreviewDeployment({ +Future _createPreviewDeployment({ required github.GitHub gitHub, required github.RepositorySlug repository, required Site site, @@ -393,10 +392,13 @@ Future _createPreviewDeployment({ if (response case {'id': final num id}) { return id.toInt(); } - return null; + throw github.GitHubError( + gitHub, + 'Failed to find a deployment id in the GitHub response.', + ); } -/// Creates a successful deployment status with [stagingUrl] as the target. +/// Creates a successful deployment status with [stagingUrl] as the preview URL. Future _createPreviewDeploymentStatus({ required github.GitHub gitHub, required github.RepositorySlug repository, @@ -421,38 +423,49 @@ Future _createPreviewDeploymentStatus({ ); } -/// Marks and deletes deployments superseded by the newly published preview. -Future _deletePreviewDeployments({ +/// Marks older successful transient preview deployments inactive. +Future _markPreviewDeploymentsInactive({ required github.GitHub gitHub, required github.RepositorySlug repository, required List deploymentIds, required String environment, }) async { - if (deploymentIds.isEmpty) { - return; - } - - print( - 'Deleting ${deploymentIds.length} older GitHub deployment(s) ' - 'for $environment...', - ); for (final deploymentId in deploymentIds) { + if (await _latestDeploymentStatusState( + gitHub: gitHub, + repository: repository, + deploymentId: deploymentId, + ) != + 'success') { + continue; + } await _createInactiveDeploymentStatus( gitHub: gitHub, repository: repository, deploymentId: deploymentId, environment: environment, ); - await gitHub.request( - 'DELETE', - '/repos/${repository.fullName}/deployments/$deploymentId', - statusCode: 204, - headers: _githubJsonHeaders, - ); } } -/// Marks [deploymentId] inactive so GitHub allows it to be deleted. +/// Returns the latest known status state for [deploymentId]. +Future _latestDeploymentStatusState({ + required github.GitHub gitHub, + required github.RepositorySlug repository, + required int deploymentId, +}) async { + final statuses = await gitHub.getJSON>( + '/repos/${repository.fullName}/deployments/$deploymentId/statuses', + params: {'per_page': '1'}, + convert: (json) => json as List, + ); + if (statuses case [{'state': final String state}, ...]) { + return state; + } + return null; +} + +/// Creates an inactive status for a superseded transient preview deployment. Future _createInactiveDeploymentStatus({ required github.GitHub gitHub, required github.RepositorySlug repository, @@ -472,12 +485,9 @@ Future _createInactiveDeploymentStatus({ ); } -Map get _githubJsonHeaders => { - 'Accept': _githubJsonMimeType, - 'X-GitHub-Api-Version': _githubApiVersion, -}; - -Map _jsonMap(Object? json) => json as Map; +/// Converts a decoded GitHub JSON object to a string-keyed map. +Map _jsonMap(Object? json) => + Map.from(json as Map); /// Builds a Firebase Hosting channel name for [site] that /// incorporates [branchOrSha] and if specified, [prNumber], @@ -502,16 +512,18 @@ String _firebaseChannelForSite( return channel.replaceAll(RegExp(r'-+$'), ''); } -/// Returns the stable GitHub environment name for a PR/site preview. +/// Returns the GitHub environment name shown for a PR/site preview. /// -/// GitHub assigns deployment ids, so the environment is the stable PR/site key. +/// The PR number keeps the deployment unique while giving GitHub's UI a +/// readable environment name. String _previewEnvironmentForSite(Site site, {required int prNumber}) => - 'preview-${site.name}-pr-$prNumber'; + 'Preview: ${site.host} (PR #$prNumber)'; -/// Returns an opaque stable payload id for a PR/site preview deployment. +/// Returns the stable payload id for a PR/site preview deployment. String _previewDeploymentPayloadId(Site site, {required int prNumber}) => '${site.name}-pr-$prNumber'; +/// Returns a shortened commit SHA for status descriptions. String _shortSha(String sha) { if (sha.length <= 7) { return sha; From ebbbc7a7ef27896639bef4d2929021d236ca94b5 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Fri, 5 Jun 2026 01:20:55 +0200 Subject: [PATCH 3/5] Try deleting old deployments --- .../lib/src/commands/stage_preview.dart | 85 ++++++++----------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/tool/dash_site/lib/src/commands/stage_preview.dart b/tool/dash_site/lib/src/commands/stage_preview.dart index 03971a9c57..1f9c59925e 100644 --- a/tool/dash_site/lib/src/commands/stage_preview.dart +++ b/tool/dash_site/lib/src/commands/stage_preview.dart @@ -258,14 +258,17 @@ String? _extractDeployedUrl(String firebaseJsonOutput) { /// The GitHub REST API version used for preview deployment calls. const String _githubApiVersion = '2026-03-10'; +/// The GitHub REST API media type used for raw deployment calls. +const String _githubJsonMimeType = 'application/vnd.github+json'; + /// The deployment task used to distinguish site previews from other deploys. const String _previewDeploymentTask = 'deploy:preview'; /// Publishes [stagingUrl] as the latest PR/site preview deployment. /// /// Each staging run creates a deployment for the staged commit. The success -/// status is followed by inactive statuses on older successful deployments for -/// the same environment. +/// status is followed by inactive statuses and deletes for older deployments +/// for the same environment. /// /// Returns `0` on success or `1` if the GitHub API call fails. Future _publishStagingDeploymentOnGitHub({ @@ -293,25 +296,25 @@ Future _publishStagingDeploymentOnGitHub({ repository: repository, environment: environment, ); - final deploymentId = await _createPreviewDeployment( - gitHub: gitHub, - repository: repository, - site: site, - stagingUrl: stagingUrl, - environment: environment, - context: context, - ); - - await _createPreviewDeploymentStatus( - gitHub: gitHub, - repository: repository, - deploymentId: deploymentId, - site: site, - stagingUrl: stagingUrl, - environment: environment, - commitSha: context.commitSha, - ); - await _markPreviewDeploymentsInactive( + // final deploymentId = await _createPreviewDeployment( + // gitHub: gitHub, + // repository: repository, + // site: site, + // stagingUrl: stagingUrl, + // environment: environment, + // context: context, + // ); + // + // await _createPreviewDeploymentStatus( + // gitHub: gitHub, + // repository: repository, + // deploymentId: deploymentId, + // site: site, + // stagingUrl: stagingUrl, + // environment: environment, + // commitSha: context.commitSha, + // ); + await _deletePreviewDeployments( gitHub: gitHub, repository: repository, deploymentIds: previousDeploymentIds, @@ -423,48 +426,29 @@ Future _createPreviewDeploymentStatus({ ); } -/// Marks older successful transient preview deployments inactive. -Future _markPreviewDeploymentsInactive({ +/// Marks older transient preview deployments inactive and deletes them. +Future _deletePreviewDeployments({ required github.GitHub gitHub, required github.RepositorySlug repository, required List deploymentIds, required String environment, }) async { for (final deploymentId in deploymentIds) { - if (await _latestDeploymentStatusState( - gitHub: gitHub, - repository: repository, - deploymentId: deploymentId, - ) != - 'success') { - continue; - } await _createInactiveDeploymentStatus( gitHub: gitHub, repository: repository, deploymentId: deploymentId, environment: environment, ); + await gitHub.request( + 'DELETE', + '/repos/${repository.fullName}/deployments/$deploymentId', + statusCode: 204, + headers: _githubJsonHeaders, + ); } } -/// Returns the latest known status state for [deploymentId]. -Future _latestDeploymentStatusState({ - required github.GitHub gitHub, - required github.RepositorySlug repository, - required int deploymentId, -}) async { - final statuses = await gitHub.getJSON>( - '/repos/${repository.fullName}/deployments/$deploymentId/statuses', - params: {'per_page': '1'}, - convert: (json) => json as List, - ); - if (statuses case [{'state': final String state}, ...]) { - return state; - } - return null; -} - /// Creates an inactive status for a superseded transient preview deployment. Future _createInactiveDeploymentStatus({ required github.GitHub gitHub, @@ -485,6 +469,11 @@ Future _createInactiveDeploymentStatus({ ); } +Map get _githubJsonHeaders => { + 'Accept': _githubJsonMimeType, + 'X-GitHub-Api-Version': _githubApiVersion, +}; + /// Converts a decoded GitHub JSON object to a string-keyed map. Map _jsonMap(Object? json) => Map.from(json as Map); From 61ff61757f1225df8b9fcf3ffcc73a075641dffd Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Fri, 5 Jun 2026 01:28:51 +0200 Subject: [PATCH 4/5] Add back creation --- .../lib/src/commands/stage_preview.dart | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tool/dash_site/lib/src/commands/stage_preview.dart b/tool/dash_site/lib/src/commands/stage_preview.dart index 1f9c59925e..0c54c13d64 100644 --- a/tool/dash_site/lib/src/commands/stage_preview.dart +++ b/tool/dash_site/lib/src/commands/stage_preview.dart @@ -296,24 +296,24 @@ Future _publishStagingDeploymentOnGitHub({ repository: repository, environment: environment, ); - // final deploymentId = await _createPreviewDeployment( - // gitHub: gitHub, - // repository: repository, - // site: site, - // stagingUrl: stagingUrl, - // environment: environment, - // context: context, - // ); - // - // await _createPreviewDeploymentStatus( - // gitHub: gitHub, - // repository: repository, - // deploymentId: deploymentId, - // site: site, - // stagingUrl: stagingUrl, - // environment: environment, - // commitSha: context.commitSha, - // ); + final deploymentId = await _createPreviewDeployment( + gitHub: gitHub, + repository: repository, + site: site, + stagingUrl: stagingUrl, + environment: environment, + context: context, + ); + + await _createPreviewDeploymentStatus( + gitHub: gitHub, + repository: repository, + deploymentId: deploymentId, + site: site, + stagingUrl: stagingUrl, + environment: environment, + commitSha: context.commitSha, + ); await _deletePreviewDeployments( gitHub: gitHub, repository: repository, From 2c71a2cfb3be892bf30a8cedf486fffdde96ecdf Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Fri, 5 Jun 2026 01:30:35 +0200 Subject: [PATCH 5/5] Handle original deployment IDs as well --- .../lib/src/commands/stage_preview.dart | 128 +++++++++++------- 1 file changed, 77 insertions(+), 51 deletions(-) diff --git a/tool/dash_site/lib/src/commands/stage_preview.dart b/tool/dash_site/lib/src/commands/stage_preview.dart index 0c54c13d64..c572bacaed 100644 --- a/tool/dash_site/lib/src/commands/stage_preview.dart +++ b/tool/dash_site/lib/src/commands/stage_preview.dart @@ -291,34 +291,36 @@ Future _publishStagingDeploymentOnGitHub({ ); try { final repository = github.RepositorySlug.full(context.repoFullName); - final previousDeploymentIds = await _findPreviewDeploymentIds( + final previousDeployments = await _findPreviewDeployments( gitHub: gitHub, repository: repository, - environment: environment, - ); - final deploymentId = await _createPreviewDeployment( - gitHub: gitHub, - repository: repository, - site: site, - stagingUrl: stagingUrl, - environment: environment, - context: context, - ); - - await _createPreviewDeploymentStatus( - gitHub: gitHub, - repository: repository, - deploymentId: deploymentId, - site: site, - stagingUrl: stagingUrl, - environment: environment, - commitSha: context.commitSha, + environments: _previewEnvironmentsForSite( + site, + prNumber: context.prNumber, + ), ); + // final deploymentId = await _createPreviewDeployment( + // gitHub: gitHub, + // repository: repository, + // site: site, + // stagingUrl: stagingUrl, + // environment: environment, + // context: context, + // ); + // + // await _createPreviewDeploymentStatus( + // gitHub: gitHub, + // repository: repository, + // deploymentId: deploymentId, + // site: site, + // stagingUrl: stagingUrl, + // environment: environment, + // commitSha: context.commitSha, + // ); await _deletePreviewDeployments( gitHub: gitHub, repository: repository, - deploymentIds: previousDeploymentIds, - environment: environment, + deployments: previousDeployments, ); } on github.GitHubError catch (error) { stderr.writeln('Error: Failed to publish the GitHub deployment: $error'); @@ -330,35 +332,45 @@ Future _publishStagingDeploymentOnGitHub({ return 0; } -/// Returns existing deployment ids for a PR/site preview [environment]. -Future> _findPreviewDeploymentIds({ +/// Returns existing deployments for the PR/site preview [environments]. +Future> _findPreviewDeployments({ required github.GitHub gitHub, required github.RepositorySlug repository, - required String environment, + required List environments, }) async { - final deploymentIds = []; - var page = 1; - while (true) { - final deployments = await gitHub.getJSON>( - '/repos/${repository.fullName}/deployments', - params: { - 'environment': environment, - 'task': _previewDeploymentTask, - 'per_page': '100', - 'page': '$page', - }, - convert: (json) => json as List, - ); - if (deployments.isEmpty) { - return deploymentIds; - } - for (final deployment in deployments) { - if (deployment case {'id': final num id}) { - deploymentIds.add(id.toInt()); + final previewDeployments = <_PreviewDeployment>[]; + final seenDeploymentIds = {}; + for (final environment in environments) { + var page = 1; + while (true) { + final deployments = await gitHub.getJSON>( + '/repos/${repository.fullName}/deployments', + params: { + 'environment': environment, + 'task': _previewDeploymentTask, + 'per_page': '100', + 'page': '$page', + }, + convert: (json) => json as List, + ); + if (deployments.isEmpty) { + break; + } + for (final deployment in deployments) { + if (deployment case {'id': final num id}) { + final deploymentId = id.toInt(); + if (seenDeploymentIds.add(deploymentId)) { + previewDeployments.add(( + id: deploymentId, + environment: environment, + )); + } + } } + page += 1; } - page += 1; } + return previewDeployments; } /// Creates the GitHub deployment record for the staged preview. @@ -430,19 +442,18 @@ Future _createPreviewDeploymentStatus({ Future _deletePreviewDeployments({ required github.GitHub gitHub, required github.RepositorySlug repository, - required List deploymentIds, - required String environment, + required List<_PreviewDeployment> deployments, }) async { - for (final deploymentId in deploymentIds) { + for (final deployment in deployments) { await _createInactiveDeploymentStatus( gitHub: gitHub, repository: repository, - deploymentId: deploymentId, - environment: environment, + deploymentId: deployment.id, + environment: deployment.environment, ); await gitHub.request( 'DELETE', - '/repos/${repository.fullName}/deployments/$deploymentId', + '/repos/${repository.fullName}/deployments/${deployment.id}', statusCode: 204, headers: _githubJsonHeaders, ); @@ -508,6 +519,18 @@ String _firebaseChannelForSite( String _previewEnvironmentForSite(Site site, {required int prNumber}) => 'Preview: ${site.host} (PR #$prNumber)'; +/// Returns all environment names to clean up for a PR/site preview. +List _previewEnvironmentsForSite(Site site, {required int prNumber}) { + return [ + _previewEnvironmentForSite(site, prNumber: prNumber), + _legacyPreviewEnvironmentForSite(site, prNumber: prNumber), + ]; +} + +/// Returns the old PR/site preview environment name used by earlier builds. +String _legacyPreviewEnvironmentForSite(Site site, {required int prNumber}) => + 'preview-${site.name}-pr-$prNumber'; + /// Returns the stable payload id for a PR/site preview deployment. String _previewDeploymentPayloadId(Site site, {required int prNumber}) => '${site.name}-pr-$prNumber'; @@ -536,3 +559,6 @@ typedef _PullRequestContext = ({ String githubToken, String commitSha, }); + +/// A deployment that should be retired from a previous preview run. +typedef _PreviewDeployment = ({int id, String environment});