Skip to content

Commit

Permalink
feat: base quickstart on remote existing target (#999)
Browse files Browse the repository at this point in the history
This PR adds the ability to bootstrap a new target using a pre-existing
specification hosted on Speakeasy's registry. The CLI will now show the
most recent 5 targets that have been interacted with (based on the CLI
event stream).

If there are any recent target candidates in the workspace, then the
user will be presented with the option to choose from an existing SDK or
create from scratch at the beginning of Quickstart. If there are no
previous generations in a workspace, then the existing "new user" flow
will be shown where the user can enter a filepath to a spec:

![CleanShot 2024-10-07 at 13 46
20@2x](https://github.com/user-attachments/assets/8cc576bc-e0bf-4a56-8ad6-5d5308fb8cae)

Then selecting from the list of recent SDKs:

![CleanShot 2024-10-07 at 13 46
32@2x](https://github.com/user-attachments/assets/6d2b44d3-f93c-4c15-b41f-5e70fb954bea)

(n.b the look and feel of the select list in the 2nd screenshot will
change / look much better once the corresponding
charmbracelet/huh#424 pr has been merged)

The end result is that a new workflow file is created that references
the existing Registry URI for the spec from the previous target

## Next steps

- Try and re-use as much of the prior target's `gen.yaml`
  • Loading branch information
adaam2 authored Nov 25, 2024
1 parent 0bda3f9 commit cdc8050
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 29 deletions.
3 changes: 2 additions & 1 deletion internal/charm/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ const AutoCompleteAnnotation = "autocomplete_extensions"

var OpenAPIFileExtensions = []string{".yaml", ".yml", ".json"}

func NewBranchPrompt(title string, output *bool) *huh.Group {
func NewBranchPrompt(title, description string, output *bool) *huh.Group {
return huh.NewGroup(huh.NewConfirm().
Title(title).
Affirmative("Yes.").
Negative("No.").
Description(description).
Value(output))
}

Expand Down
2 changes: 1 addition & 1 deletion internal/interactivity/simpleConfirm.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ func SimpleConfirm(message string, defaultValue bool) bool {
confirm := defaultValue

if _, err := charm_internal.NewForm(
huh.NewForm(charm_internal.NewBranchPrompt(message, &confirm)),
huh.NewForm(charm_internal.NewBranchPrompt(message, "", &confirm)),
).
ExecuteForm(); err != nil {
return false
Expand Down
181 changes: 181 additions & 0 deletions internal/remote/sources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package remote

import (
"context"
"fmt"
"sort"
"time"

"github.com/samber/lo"
core "github.com/speakeasy-api/speakeasy-core/auth"
"github.com/speakeasy-api/speakeasy/internal/sdk"

speakeasyclientsdkgo "github.com/speakeasy-api/speakeasy-client-sdk-go/v3"
"github.com/speakeasy-api/speakeasy-client-sdk-go/v3/pkg/models/operations"
"github.com/speakeasy-api/speakeasy-client-sdk-go/v3/pkg/models/shared"
)

// RecentGeneration represents a recent generation of a target in the workspace.
// The source of this data is our CLI event stream, which is updated every time
// a target is generated.
type RecentGeneration struct {
CreatedAt time.Time
ID string
TargetName string
Target string
SourceNamespace string
Success bool
Published bool
RegistryUri string

// May not be set
GitRepoOrg *string
GitRepo *string

// gen.yaml
GenerateConfig *string
}

const (
// The event stream contains multiple events for the same namespace, so we want to
// break execution once we've seen a minimum, arbitrary number of unique namespaces.
recentGenerationsToShow int = 5
)

// GetRecentWorkspaceGenerations returns the most recent generations of targets in a workspace
// This is based on the CLi event stream, which is updated on every CLI interaction.
func GetRecentWorkspaceGenerations(ctx context.Context) ([]RecentGeneration, error) {
workspaceId, err := core.GetWorkspaceIDFromContext(ctx)
if err != nil {
return nil, err
}

speakeasyClient, err := sdk.InitSDK()
if err != nil {
return nil, err
}

// The event stream is limited to the most recent 250 events
res, err := speakeasyClient.Events.GetTargets(ctx, operations.GetWorkspaceTargetsRequest{})

if err != nil {
return nil, err
}

if len(res.TargetSDKList) == 0 {
return nil, fmt.Errorf("no events found for workspace %s", workspaceId)
}

seenUniqueNamespaces := make(map[string]bool)

var generations []RecentGeneration

// sort by most recent
sort.Slice(res.TargetSDKList, func(i, j int) bool {
return res.TargetSDKList[i].LastEventCreatedAt.After(res.TargetSDKList[j].LastEventCreatedAt)
})

for _, target := range res.TargetSDKList {
// Filter out cli events that aren't generation based, or lack the required
// fields.
if !isRelevantGenerationTarget(target) {
continue
}

if seenUniqueNamespaces[*target.SourceNamespaceName] {
continue
}

if !hasMainRevision(ctx, speakeasyClient, *target.SourceNamespaceName) {
continue
}

seenUniqueNamespaces[*target.SourceNamespaceName] = true

registryUri, err := GetRegistryUriForSource(ctx, *target.SourceNamespaceName)
if err != nil {
return nil, err
}

generations = append(generations, RecentGeneration{
ID: target.ID,
CreatedAt: target.LastEventCreatedAt,
TargetName: *target.GenerateTargetName,
Target: target.GenerateTarget,
GitRepoOrg: target.GhActionOrganization,
GitRepo: target.GhActionRepository,
SourceNamespace: *target.SourceNamespaceName,
GenerateConfig: target.GenerateConfigPostVersion,
RegistryUri: registryUri,
Success: *target.Success,
})

if len(seenUniqueNamespaces) >= recentGenerationsToShow {
break
}
}

return generations, nil
}

func isRelevantGenerationTarget(target shared.TargetSDK) bool {
if target.GenerateTarget == "" {
return false
}
if target.GhActionRunLink == nil {
return false
}
if target.GhActionOrganization == nil || target.GhActionRepository == nil ||
*target.GhActionOrganization == "" || *target.GhActionRepository == "" {
return false
}

if target.GenerateTargetName == nil {
return false
}
if target.SourceNamespaceName == nil {
return false
}

return true
}

const (
mainRevisionTag = "main"
)

func hasMainRevision(ctx context.Context, client *speakeasyclientsdkgo.Speakeasy, namespace string) bool {
revisions, err := client.Artifacts.GetRevisions(ctx, operations.GetRevisionsRequest{
NamespaceName: namespace,
})

if err != nil {
return false
}

if len(revisions.GetRevisionsResponse.GetItems()) == 0 {
return false
}

foundMainTag := false

for _, revision := range revisions.GetRevisionsResponse.GetItems() {
if lo.Contains(revision.GetTags(), mainRevisionTag) {
foundMainTag = true
break
}
}

return foundMainTag
}

func GetRegistryUriForSource(ctx context.Context, sourceNamespace string) (string, error) {
orgSlug := core.GetOrgSlugFromContext(ctx)
workspaceSlug := core.GetWorkspaceSlugFromContext(ctx)

if orgSlug == "" || workspaceSlug == "" {
return "", fmt.Errorf("could not generate registry uri: missing organization or workspace slug")
}

return fmt.Sprintf("registry.speakeasyapi.dev/%s/%s/%s:main", orgSlug, workspaceSlug, sourceNamespace), nil
}
Loading

0 comments on commit cdc8050

Please sign in to comment.