Skip to content

Conversation

@adambenhassen
Copy link
Collaborator

@adambenhassen adambenhassen commented Dec 16, 2025

Description

Adds protection settings for app deployment retirement to prevent accidental removal of actively-used deployments.

How It Works

  1. When retireAppDeployment is called and protection is enabled:
    • If no usage data exists -> retirement allowed (deployment was never used)
    • If usage exists, checks both criteria:
      • Days inactive >= minDaysInactive
      • Traffic percentage <= maxTrafficPercentage
    • If either check fails -> retirement blocked with error
  2. Using force: true (or --force in CLI) bypasses all checks

Settings page

Screenshot 2025-12-16 at 12 15 46

Closes CONSOLE-1501

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @adambenhassen, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a significant enhancement to app deployment management by introducing a robust retirement protection mechanism. It empowers users to safeguard active app deployments from inadvertent retirement through configurable rules based on usage patterns. The changes span across the API, CLI, and UI, providing a complete solution for managing and overriding these protections, backed by necessary database schema updates and comprehensive testing.

Highlights

  • App Deployment Retirement Protection: Introduced a new feature that allows users to configure protection rules for app deployments, preventing accidental retirement of active deployments based on inactivity duration and traffic percentage.
  • CLI --force Flag: Added a --force flag to the app:retire CLI command, enabling users to bypass the configured retirement protection rules when necessary.
  • Detailed Protection Error Responses: The API now returns specific protectionDetails within the RetireAppDeploymentError when a retirement attempt is blocked by protection rules, providing clear reasons for the blockage.
  • UI for Protection Configuration: Implemented a new section in the target settings UI to allow users to enable, disable, and configure the app deployment protection settings, including minimum inactive days and maximum traffic percentage.
  • Database Migrations: New database columns have been added to the targets table to store the app deployment protection configuration, ensuring persistence of these settings.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a protection mechanism to prevent accidental retirement of in-use application deployments. The changes are comprehensive, spanning the database, backend API, CLI, and frontend UI. The implementation includes new GraphQL endpoints for configuration, enhanced error reporting with protection details, and a new settings page. The test coverage for both API and CLI scenarios is thorough. I have a couple of suggestions for improvement, one related to our architectural patterns for data access and another to make error messages more informative when multiple protection rules are violated.

@adambenhassen adambenhassen requested a review from jdolle December 16, 2025 17:42
@adambenhassen adambenhassen force-pushed the adam/retire-app-deployment-protection branch from 0a7bcdc to ae97c6d Compare December 16, 2025 17:46
@github-actions
Copy link
Contributor

github-actions bot commented Dec 16, 2025

🚀 Snapshot Release (alpha)

The latest changes of this PR are available as alpha on npm (based on the declared changesets):

Package Version Info
@graphql-hive/apollo 0.46.0-alpha-20251217165442-a06fbf10adf650bd431c2d236058f599b54c9f19 npm ↗︎ unpkg ↗︎
@graphql-hive/cli 0.56.1-alpha-20251217165442-a06fbf10adf650bd431c2d236058f599b54c9f19 npm ↗︎ unpkg ↗︎
@graphql-hive/core 0.19.0-alpha-20251217165442-a06fbf10adf650bd431c2d236058f599b54c9f19 npm ↗︎ unpkg ↗︎
@graphql-hive/envelop 0.40.1-alpha-20251217165442-a06fbf10adf650bd431c2d236058f599b54c9f19 npm ↗︎ unpkg ↗︎
@graphql-hive/yoga 0.46.1-alpha-20251217165442-a06fbf10adf650bd431c2d236058f599b54c9f19 npm ↗︎ unpkg ↗︎
hive 8.14.0-alpha-20251217165442-a06fbf10adf650bd431c2d236058f599b54c9f19 npm ↗︎ unpkg ↗︎
hive-apollo-router-plugin 2.3.6-alpha-20251217165442-a06fbf10adf650bd431c2d236058f599b54c9f19 npm ↗︎ unpkg ↗︎
hive-console-sdk-rs 0.2.3-alpha-20251217165442-a06fbf10adf650bd431c2d236058f599b54c9f19 npm ↗︎ unpkg ↗︎

@github-actions
Copy link
Contributor

github-actions bot commented Dec 16, 2025

📚 Storybook Deployment

The latest changes are available as preview in: https://pr-7432.hive-storybook.pages.dev

@github-actions
Copy link
Contributor

github-actions bot commented Dec 16, 2025

💻 Website Preview

The latest changes are available as preview in: https://pr-7432.hive-landing-page.pages.dev

@github-actions
Copy link
Contributor

github-actions bot commented Dec 16, 2025

🐋 This PR was built and pushed to the following Docker images:

Targets: build

Platforms: linux/amd64

Image Tag: a06fbf10adf650bd431c2d236058f599b54c9f19


const targetSlug = `${organization.slug}/${project.slug}/${target.slug}`;

// CLI retire without --force should fail
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice job on these tests. I appreciate that you checked that it would fail without force before checking force passes.

this.log(` Current traffic: ${details.currentTrafficPercentage.toFixed(2)}%`);
}
this.log(` Max traffic threshold: ${details.maxTrafficPercentage}%`);
this.log('\nUse --force to bypass protection.\n');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again great job. You provide the important details and explain how they can bypass.

name: '2025.12.15T00-00-00.app-deployment-protection.ts',
run: ({ sql }) => sql`
ALTER TABLE targets ADD COLUMN app_deployment_protection_enabled BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE targets ADD COLUMN app_deployment_protection_min_days_inactive INT NOT NULL DEFAULT 30;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This may get explained as I review the rest of the PR. But how do the min days work with our usage retention_in_days limit? E.g. if we retain only 7 days but have min_days_inactive as 30, then after 7 days of not being used will we think this is satisfied?

targetId: args.targetId,
appName: args.appDeployment.name,
appVersion: args.appDeployment.version,
periodDays: 30,
Copy link
Collaborator

Choose a reason for hiding this comment

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

should this be configurable also?

WHERE
"target" = ${args.targetId}
AND "timestamp" >= toDateTime(${formatClickHouseDate(periodFrom)}, 'UTC')
AND "timestamp" <= toDateTime(${formatClickHouseDate(periodTo)}, 'UTC')
Copy link
Collaborator

Choose a reason for hiding this comment

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

This utility method seems like a lot of work that the client or clickhouse should handle for us.

I dug a little into clickhouse docs and I think a better approach is to use parseDateTimeBestEffort(${periodFrom.toISOString()}). parseDateTimeBestEffort handles ISO format strings and toISOString always returns UTC, so it should be equivalent.

AND "timestamp" <= toDateTime(${formatClickHouseDate(periodTo)}, 'UTC')
`,
queryId: 'get-app-deployment-traffic-percentage',
timeout: 20_000,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is 20s a default we use? My preference is to timeout quicker because 20s seems like a long time. If it's waiting that long then it's likely going to fail.

Update the app deployment protection configuration of a target.
"""
updateTargetAppDeploymentProtectionConfiguration(
input: UpdateTargetAppDeploymentProtectionConfigurationInput! @tag(name: "public")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor: I prefer keeping new APIs private until we're sold on them. But if a customer is asking for this -- public is 👍

"""
Input for updating app deployment protection configuration.
Fields not provided (omitted) will retain the previous value.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we error or return OK if no input fields are provided?


const selector = await this.idTranslator.resolveTargetReference({ reference: args.target });
if (!selector) {
this.session.raise('target:modifySettings');
Copy link
Collaborator

Choose a reason for hiding this comment

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

should the assertion be before the selection?
If you assert target:modifySettings before resolveTargetReference, then we can correctly return a more accurate error like "target doesnt exist"

},
validationSchema: Yup.object().shape({
minDaysInactive: Yup.number()
.min(0, 'Must be at least 0')
Copy link
Collaborator

Choose a reason for hiding this comment

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

should a max be determined by retentionInDays?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants