Skip to content

feat: add POST /projects/{id}/tags endpoint to create deployment tags#1772

Draft
adaam2 wants to merge 2 commits intomainfrom
feat/create-tag-endpoint
Draft

feat: add POST /projects/{id}/tags endpoint to create deployment tags#1772
adaam2 wants to merge 2 commits intomainfrom
feat/create-tag-endpoint

Conversation

@adaam2
Copy link
Member

@adaam2 adaam2 commented Mar 4, 2026

Summary

Implements Linear AGE-1465: Create endpoint for adding deployment tags to projects.

This PR adds a new POST /rpc/projects.createDeploymentTag endpoint that allows creating named tags pointing to specific deployments within a project.

Changes

  • Goa Design: Added DeploymentTag type and createDeploymentTag method with validation
    • Tag names must be 1-60 characters, alphanumeric with hyphens and dots allowed
    • Pattern validation: ^[a-zA-Z0-9][a-zA-Z0-9.\-]*$
  • Service Implementation: Created tag creation logic with proper error handling
    • Validates deployment exists and belongs to project
    • Creates tag record and initial history entry in transaction
    • Handles duplicate tag names with CodeConflict error
  • Database Queries: Added SQLc queries for tag creation and deployment verification
  • Comprehensive Tests: 15 test cases covering:
    • Success scenarios (valid names, dots/hyphens, max length)
    • Validation (empty names, too long, invalid characters)
    • Authorization (missing auth context)
    • Business logic (deployment not found, wrong project, duplicates)
    • Database persistence verification

Testing

All tests pass:

go test -v -run "TestProjectsService_CreateDeploymentTag" ./internal/projects/...

Test coverage includes:

  • ✅ Creates deployment tag successfully
  • ✅ Creates history entry on tag creation
  • ✅ Accepts valid tag names (main, latest, v1.0.0, v1.2.3-beta, etc.)
  • ✅ Rejects without auth context
  • ✅ Rejects invalid deployment ID format
  • ✅ Rejects when deployment does not exist
  • ✅ Rejects when deployment belongs to different project
  • ✅ Rejects duplicate tag name for same project
  • ✅ Allows same tag name in different projects
  • ✅ Allows multiple tags pointing to same deployment
  • ✅ Rejects empty tag name
  • ✅ Rejects tag name that is too long (>60 chars)
  • ✅ Rejects tag names with invalid characters
  • ✅ Accepts tag name at max length boundary (60 chars)
  • ✅ Stores tag correctly in database

🤖 Generated with Claude Code


Open with Devin

Implements Linear issue AGE-1465 with comprehensive validation and tests.

- Add DeploymentTag type to Goa design with proper validation
- Create createDeploymentTag endpoint accepting name and deployment_id
- Validate tag name format (alphanumeric, hyphens, dots, max 60 chars)
- Ensure deployment exists and belongs to project
- Create tag and history entry in single transaction
- Return tag with deployment details

Tests cover success cases, validation, auth, duplicates, and DB constraints.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@adaam2 adaam2 requested a review from a team as a code owner March 4, 2026 13:23
@vercel
Copy link

vercel bot commented Mar 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gram-docs-redirect Ready Ready Preview, Comment Mar 4, 2026 1:27pm

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Mar 4, 2026

⚠️ No Changeset found

Latest commit: fd36782

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Generated by Speakeasy pre-commit hook from OpenAPI changes.
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +358 to +384
// Create the deployment tag
tag, err := s.repo.CreateDeploymentTag(ctx, repo.CreateDeploymentTagParams{
ProjectID: *authCtx.ProjectID,
DeploymentID: uuid.NullUUID{UUID: deploymentID, Valid: true},
Name: payload.Name,
})
var pgErr *pgconn.PgError
switch {
case errors.As(err, &pgErr):
if pgErr.Code == pgerrcode.UniqueViolation {
return nil, oops.E(oops.CodeConflict, err, "tag name already exists for this project")
}
return nil, oops.E(oops.CodeUnexpected, err, "database error creating deployment tag").Log(ctx, s.logger, attr.SlogProjectID(authCtx.ProjectID.String()))
case err != nil:
return nil, oops.E(oops.CodeUnexpected, err, "unexpected error creating deployment tag").Log(ctx, s.logger, attr.SlogProjectID(authCtx.ProjectID.String()))
}

// Create history entry for the initial tag creation
err = s.repo.CreateDeploymentTagHistoryEntry(ctx, repo.CreateDeploymentTagHistoryEntryParams{
TagID: tag.ID,
PreviousDeploymentID: uuid.NullUUID{UUID: uuid.Nil, Valid: false},
NewDeploymentID: uuid.NullUUID{UUID: deploymentID, Valid: true},
ChangedBy: conv.ToPGText(authCtx.UserID),
})
if err != nil {
return nil, oops.E(oops.CodeUnexpected, err, "error creating tag history entry").Log(ctx, s.logger)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Tag creation and history entry are not wrapped in a transaction, causing data inconsistency on partial failure

The CreateDeploymentTag method performs two sequential database writes — inserting the tag (CreateDeploymentTag) and inserting the history entry (CreateDeploymentTagHistoryEntry) — without wrapping them in a transaction. If the second insert fails, the tag is already committed to the database, but the error is returned to the caller.

Root Cause and Impact

At server/internal/projects/impl.go:358-383, the tag is created at line 359 and committed immediately (auto-commit), then the history entry is created at line 376. If the history entry insert fails (e.g., transient DB error, or FK constraint violation on changed_by), the tag persists in the database but the caller receives an error.

This causes two problems:

  1. Orphaned tag without history: The tag exists but has no corresponding audit trail entry in deployment_tag_history, violating the data integrity invariant that every tag creation should have a history record.
  2. Retry causes conflict: Since the tag was already committed, if the user retries the request they will get a CodeConflict (409) error due to the unique constraint on (project_id, name) at server/database/schema.sql:1125-1126, making the operation appear permanently broken.

Other services in this codebase use s.db.Begin(ctx) + s.repo.WithTx(dbtx) for multi-write operations (e.g., server/internal/toolsets/impl.go, server/internal/thirdparty/slack/impl.go). The projects repo already has WithTx available (server/internal/projects/repo/db.go).

Prompt for agents
In server/internal/projects/impl.go, the CreateDeploymentTag method (lines 335-396) needs to wrap the two database writes in a transaction. Specifically:

1. Before the CreateDeploymentTag repo call (line 359), start a transaction:
   dbtx, err := s.db.Begin(ctx)
   if err != nil { return nil, oops.E(oops.CodeUnexpected, err, "error starting transaction").Log(ctx, s.logger) }
   defer dbtx.Rollback(ctx)
   txRepo := s.repo.WithTx(dbtx)

2. Replace s.repo.CreateDeploymentTag (line 359) with txRepo.CreateDeploymentTag
3. Replace s.repo.CreateDeploymentTagHistoryEntry (line 376) with txRepo.CreateDeploymentTagHistoryEntry
4. After the history entry is successfully created (after line 383), commit the transaction:
   if err := dbtx.Commit(ctx); err != nil { return nil, oops.E(oops.CodeUnexpected, err, "error committing transaction").Log(ctx, s.logger) }

This matches the pattern used in other services like server/internal/toolsets/impl.go and server/internal/thirdparty/slack/impl.go.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@adaam2 adaam2 marked this pull request as draft March 4, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant