Skip to content

Commit

Permalink
feat: add -t, --push-tags flag to deploy
Browse files Browse the repository at this point in the history
If flag is provided, gamma will verify no tag exists and tag the commit based on the package.version field of each action.

Additionally, a new `check-versions` command is added to verify version does not exist yet for changed versions in a PR (should be squashed commit to verify the PR).
  • Loading branch information
vincenthsh committed May 11, 2024
1 parent 999bf96 commit d6ede77
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 15 deletions.
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(cmd *cobra.Command, args []string) {

Check warning on line 24 in cmd/checkversions/checkversions.go

View workflow job for this annotation

GitHub Actions / Lint files

unused-parameter: parameter 'cmd' seems to be unused, consider removing or renaming it as _ (revive)

Check warning on line 24 in cmd/checkversions/checkversions.go

View workflow job for this annotation

GitHub Actions / Lint files

unused-parameter: parameter 'args' seems to be unused, consider removing or renaming it as _ (revive)
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); 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, err error) error {

Check failure on line 270 in internal/git/git.go

View workflow job for this annotation

GitHub Actions / Lint files

SA4009: argument err is overwritten before first use (staticcheck)
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)

Check failure on line 278 in internal/git/git.go

View workflow job for this annotation

GitHub Actions / Lint files

SA4009(related information): assignment to err (staticcheck)
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
}

0 comments on commit d6ede77

Please sign in to comment.