Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add -t, --push-tags flag to deploy #10

Merged
merged 1 commit into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ Gamma is a tool that sets out to solve a few shortcomings when it comes to manag
- 🚀 Automatically build all your actions into individual, publishable repos
- 🚀 Share schema definitions between actions
- 🚀 Version all actions separately
- 🚀 Optionally push version tags

Gamma allows you to have a monorepo of actions that are then built and deployed into individual repos. Having each action in its own repo allows for the action to be published on the Github Marketplace.

Gamma also goes further when it comes to sharing common `action.yml` attributes between actions. Actions in your monorepo can extend upon other YAML files and bring in their `inputs`, `branding`, etc - reducing code duplication and making things easier to maintain.

## How to use

This assumes you're using `yarn` with workspaces. Each workspace is an action.
This assumes you're using `pnpm` with workspaces and `nx` for caching. Each workspace is an action.

Your root `package.json` should look like:
A good monorepo bootstrapper for `pnpm` and `nx` is [@aws/pdk - monorepoTs](https://aws.github.io/aws-pdk/developer_guides/monorepo/index.html) project type.

Your root `package.json` will look like:

```json
{
Expand All @@ -42,7 +45,7 @@ Your root `package.json` should look like:

Each action then lives under the `actions/` directory.

Each action should be able to be built via `yarn build`. We recommend [ncc](https://github.com/vercel/ncc) for building your actions. The compiled source code should end up in a `dist` folder, relative to the action. You should add `dist/` to your `.gitignore`.
Each action should be able to be built via `pnpm exec nx run <packageName>:build`. We recommend [ncc](https://github.com/vercel/ncc) for building your actions. The compiled source code should end up in a `dist` folder, relative to the action. You should add `dist/` to your `.gitignore`.

`actions/example/package.json`

Expand Down Expand Up @@ -99,7 +102,7 @@ branding:
icon: terminal
color: purple
runs:
using: node16
using: node20
main: dist/index.js
```

Expand All @@ -116,7 +119,7 @@ inputs:
description: Specify the version without the preceding "v"
required: true
runs:
using: node16
using: node20
main: dist/index.js
branding:
icon: terminal
Expand Down
132 changes: 132 additions & 0 deletions cmd/checkversions/checkversions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package checkversions

import (
"os"
"strings"
"time"

"github.com/jedib0t/go-pretty/v6/text"
"github.com/spf13/cobra"

"github.com/gravitational/gamma/internal/action"
"github.com/gravitational/gamma/internal/git"
"github.com/gravitational/gamma/internal/logger"
"github.com/gravitational/gamma/internal/utils"
"github.com/gravitational/gamma/internal/workspace"
)

var workingDirectory string

var Command = &cobra.Command{
Use: "check-versions",
Short: "Check versions of changed actions in the monorepo",
Long: `Finds all changed actions and verifies no tag exists for their current version.`,
Run: func(_ *cobra.Command, _ []string) {
started := time.Now()

if workingDirectory == "the current working directory" { // this is the default value from the flag
wd, err := os.Getwd()
if err != nil {
logger.Fatalf("could not get current working directory: %v", err)
}

workingDirectory = wd
}

// TODO: clean this up
outputDirectory := "build" // ignored
wd, od, err := utils.NormalizeDirectories(workingDirectory, outputDirectory)
if err != nil {
logger.Fatal(err)
}

repo, err := git.New(wd)
if err != nil {
logger.Fatal(err)
}

logger.Info("collecting changed files")

changed, err := repo.GetChangedFiles()
if err != nil {
logger.Fatal(err)
}

logger.Infof("files changed [%s]", strings.Join(changed, ", "))

ws := workspace.New(wd, od)

logger.Info("collecting actions")

actions, err := ws.CollectActions()
if err != nil {
logger.Fatal(err)
}

if len(actions) == 0 {
logger.Fatal("could not find any actions")
}

var actionNames []string
for _, action := range actions {
actionNames = append(actionNames, action.Name())
}

logger.Infof("found actions [%s]", strings.Join(actionNames, ", "))

var actionsToVerify []action.Action

outer:
for _, action := range actions {
for _, file := range changed {
if action.Contains(file) {
actionsToVerify = append(actionsToVerify, action)

continue outer
}
}
}
if len(actionsToVerify) == 0 {
logger.Warning("no actions have changed, exiting")

return
}

var hasError bool

for _, action := range actionsToVerify {
logger.Infof("action %s has changes, verifying version", action.Name())

verifyStarted := time.Now()

if exists, err := repo.TagExists(action); err != nil || exists {
hasError = true
if err != nil {
logger.Errorf("error verifying action %s: %v", action.Name(), err)
}
if exists {
logger.Errorf("version %s@v%s already exists", action.Name(), action.Version())
continue
}
}

verifyTook := time.Since(verifyStarted)

logger.Successf("successfully verified action %s@v%s in %.2fs", action.Name(), action.Version(), verifyTook.Seconds())
}

bold := text.Colors{text.FgWhite, text.Bold}

took := time.Since(started)

if hasError {
logger.Fatal(bold.Sprintf("completed with errors in %.2fs", took.Seconds()))
}

logger.Success(bold.Sprintf("done in %.2fs", took.Seconds()))
},
}

func init() {
Command.Flags().StringVarP(&workingDirectory, "directory", "d", "the current working directory", "directory containing the monorepo of actions")
}
4 changes: 3 additions & 1 deletion cmd/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

var outputDirectory string
var workingDirectory string
var pushTags *bool
var assetPaths []string

var Command = &cobra.Command{
Expand Down Expand Up @@ -123,7 +124,7 @@ var Command = &cobra.Command{

deployStarted := time.Now()

if err := repo.DeployAction(action); err != nil {
if err := repo.DeployAction(action, *pushTags); err != nil {
hasError = true
logger.Errorf("error deploying action %s: %v", action.Name(), err)

Expand All @@ -150,5 +151,6 @@ var Command = &cobra.Command{
func init() {
Command.Flags().StringVarP(&outputDirectory, "output", "o", "build", "output directory")
Command.Flags().StringVarP(&workingDirectory, "directory", "d", "the current working directory", "directory containing the monorepo of actions")
pushTags = Command.Flags().BoolP("push-tags", "t", false, "also the version of action as tag")
Command.Flags().StringArrayVarP(&assetPaths, "asset", "a", []string{}, "copy over an asset to each action")
}
6 changes: 6 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/spf13/cobra"

"github.com/gravitational/gamma/cmd/build"
"github.com/gravitational/gamma/cmd/checkversions"
"github.com/gravitational/gamma/cmd/deploy"
"github.com/gravitational/gamma/internal/color"
)
Expand Down Expand Up @@ -32,6 +33,7 @@ func init() {
cobra.AddTemplateFunc("logo", logo)

rootCmd.AddCommand(build.Command)
rootCmd.AddCommand(checkversions.Command)
rootCmd.AddCommand(deploy.Command)

rootCmd.SetHelpTemplate(`{{ logo }}
Expand Down Expand Up @@ -76,6 +78,8 @@ func colorize(s, name string) string {
switch s {
case build.Command.Name():
return color.Magenta(name)
case checkversions.Command.Name():
return color.Purple(name)
case deploy.Command.Name():
return color.Teal(name)
case "help":
Expand All @@ -91,6 +95,8 @@ func emoji(s string) string {
switch s {
case build.Command.Name():
return "🔧"
case checkversions.Command.Name():
return "🔍"
case deploy.Command.Name():
return "🚀"
case "help":
Expand Down
5 changes: 5 additions & 0 deletions internal/action/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Config struct {
type Action interface {
Build() error
Name() string
Version() string
Owner() string
RepoName() string
OutputDirectory() string
Expand Down Expand Up @@ -69,6 +70,10 @@ func (a *action) Name() string {
return a.packageInfo.Name
}

func (a *action) Version() string {
return a.packageInfo.Version
}

func (a *action) RepoName() string {
return a.repoName
}
Expand Down
78 changes: 69 additions & 9 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import (

type Git interface {
GetChangedFiles() ([]string, error)
DeployAction(a action.Action) error
TagExists(a action.Action) (bool, error)
DeployAction(a action.Action, pushTags bool) error
}

type git struct {
Expand Down Expand Up @@ -74,6 +75,22 @@ func createGithubClient() (*github.Client, error) {
return github.NewClient(&http.Client{Transport: itr}), nil
}

func (g *git) TagExists(a action.Action) (bool, error) {
tags, _, err := g.gh.Repositories.ListTags(context.Background(), a.Owner(), a.RepoName(), nil)
if err != nil {
return false, fmt.Errorf("could not fetch tags: %v", err)
}

// iterate over all tags, return true if the tag exists
for _, t := range tags {
if *t.Name == fmt.Sprintf("v%s", a.Version()) {
return true, nil
}
}

return false, nil
}

func (g *git) GetChangedFiles() ([]string, error) {
head, err := g.repo.Head()
if err != nil {
Expand Down Expand Up @@ -117,21 +134,40 @@ func (g *git) GetChangedFiles() ([]string, error) {
return files, nil
}

func (g *git) DeployAction(a action.Action) error {
func (g *git) DeployAction(a action.Action, pushTags bool) error {
ref, err := g.getRef(context.Background(), a)
if err != nil {
return fmt.Errorf("could not create git ref: %v", err)
}

if pushTags {
// make sure tag doesn't already exist
tagExists, err := g.TagExists(a)
if err != nil {
return fmt.Errorf("could not verify if tag exists: %v", err)
}

if tagExists {
return fmt.Errorf("tag already exists: v%v", a.Version())
}
}

tree, err := g.getTree(context.Background(), ref, a)
if err != nil {
return fmt.Errorf("could not create git tree: %v", err)
}

if err := g.pushCommit(context.Background(), ref, tree, a); err != nil {
newCommit, err := g.pushCommit(context.Background(), ref, tree, a)
if err != nil {
return fmt.Errorf("could not push changes: %v", err)
}

if pushTags {
if err := g.pushTag(context.Background(), a, newCommit); err != nil {
return fmt.Errorf("could not push tag: %v", err)
}
}

return nil
}

Expand Down Expand Up @@ -193,22 +229,22 @@ func (g *git) getRef(ctx context.Context, a action.Action) (*github.Reference, e
return ref, nil
}

func (g *git) pushCommit(ctx context.Context, ref *github.Reference, tree *github.Tree, a action.Action) error {
func (g *git) pushCommit(ctx context.Context, ref *github.Reference, tree *github.Tree, a action.Action) (*github.Commit, error) {
parent, _, err := g.gh.Repositories.GetCommit(ctx, a.Owner(), a.RepoName(), *ref.Object.SHA, nil)
if err != nil {
return err
return nil, err
}

parent.Commit.SHA = parent.SHA

head, err := g.repo.Head()
if err != nil {
return fmt.Errorf("could not get HEAD: %v", err)
return nil, fmt.Errorf("could not get HEAD: %v", err)
}

c, err := g.repo.CommitObject(head.Hash())
if err != nil {
return fmt.Errorf("could not get the HEAD commit: %v", err)
return nil, fmt.Errorf("could not get the HEAD commit: %v", err)
}

commit := &github.Commit{
Expand All @@ -219,11 +255,35 @@ func (g *git) pushCommit(ctx context.Context, ref *github.Reference, tree *githu

newCommit, _, err := g.gh.Git.CreateCommit(ctx, a.Owner(), a.RepoName(), commit)
if err != nil {
return err
return nil, err
}

ref.Object.SHA = newCommit.SHA
_, _, err = g.gh.Git.UpdateRef(ctx, a.Owner(), a.RepoName(), ref, false)
if err != nil {
return nil, err
}

return newCommit, nil
}

return err
func (g *git) pushTag(ctx context.Context, a action.Action, newCommit *github.Commit) error {
tagString := fmt.Sprintf("v%v", a.Version())
tag := &github.Tag{
Tag: github.String(tagString),
Message: github.String(fmt.Sprintf("Tag for version %s", a.Version())),
Object: &github.GitObject{SHA: github.String(*newCommit.SHA), Type: github.String("commit")},
}

_, _, err := g.gh.Git.CreateTag(ctx, a.Owner(), a.RepoName(), tag)
if err != nil {
return fmt.Errorf("could not create the tag: %v", err)
}

refTag := &github.Reference{Ref: github.String("refs/tags/" + tagString), Object: &github.GitObject{SHA: github.String(*newCommit.SHA)}}
_, _, err = g.gh.Git.CreateRef(ctx, a.Owner(), a.RepoName(), refTag)
if err != nil {
return fmt.Errorf("could not create the reference for tag: %v", err)
}
return nil
}
Loading