diff --git a/.github/workflows/ship_it.yml b/.github/workflows/ship_it.yml new file mode 100644 index 0000000..e55e6ea --- /dev/null +++ b/.github/workflows/ship_it.yml @@ -0,0 +1,33 @@ +name: "Ship It!" + +concurrency: + # There should only be able one running job per repository / branch combo. + # We do not want multiple deploys running in parallel. + group: ${{ github.repository }}-${{ github.ref_name }} + +on: + push: + branches: + - 'master' + - 'containerize' + pull_request: + workflow_dispatch: + +jobs: + dagger: + runs-on: ubuntu-latest + steps: + - name: "Checkout code..." + uses: actions/checkout@v3 + + - name: "Setup Go..." + uses: actions/setup-go@v4 + with: + go-version: "1.20" + + - name: "Ship it!" + env: + GHCR_USERNAME: "${{ vars.GHCR_USERNAME }}" + GHCR_PASSWORD: "${{ secrets.GHCR_PASSWORD }}" + run: | + go run main.go image diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e5d02c1 --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module github.com/thechangelog/nightly + +go 1.20 + +require ( + dagger.io/dagger v0.8.7 + github.com/urfave/cli/v2 v2.25.7 +) + +require ( + github.com/99designs/gqlgen v0.17.31 // indirect + github.com/Khan/genqlient v0.6.0 // indirect + github.com/adrg/xdg v0.4.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/vektah/gqlparser/v2 v2.5.6 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.12.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4d2d87e --- /dev/null +++ b/go.sum @@ -0,0 +1,53 @@ +dagger.io/dagger v0.8.7 h1:3wGzK9RKjLcNk5AnIYqkO7TzIJyftb8fT+h0WM9chAM= +dagger.io/dagger v0.8.7/go.mod h1:DbJi6aSXaRLuio0lHlnpNxfuAL5uMJvRy4UIytmbtLo= +github.com/99designs/gqlgen v0.17.31 h1:VncSQ82VxieHkea8tz11p7h/zSbvHSxSDZfywqWt158= +github.com/99designs/gqlgen v0.17.31/go.mod h1:i4rEatMrzzu6RXaHydq1nmEPZkb3bKQsnxNRHS4DQB4= +github.com/Khan/genqlient v0.6.0 h1:Bwb1170ekuNIVIwTJEqvO8y7RxBxXu639VJOkKSrwAk= +github.com/Khan/genqlient v0.6.0/go.mod h1:rvChwWVTqXhiapdhLDV4bp9tz/Xvtewwkon4DpWWCRM= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= +github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU= +github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..69a30bd --- /dev/null +++ b/main.go @@ -0,0 +1,222 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "time" + + "dagger.io/dagger" + "github.com/urfave/cli/v2" +) + +func main() { + ctx := context.Background() + dag, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr)) + if err != nil { + panic(err) + } + defer dag.Close() + + app := &cli.App{ + Name: "nightly", + Usage: "Changelog Nightly CI/CD pipeline entrypoints", + Version: "v2023.09.30", + Compiled: time.Now(), + Authors: []*cli.Author{ + { + Name: "Gerhard Lazu", + Email: "gerhard@changelog.com", + }, + }, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "nocache", + Aliases: []string{"n"}, + Usage: "Bust Dagger ops cache", + EnvVars: []string{"NOCACHE"}, + }, + &cli.BoolFlag{ + Name: "debug", + Aliases: []string{"d"}, + Usage: "Debug command", + EnvVars: []string{"DEBUG"}, + }, + &cli.StringFlag{ + Name: "platform", + Aliases: []string{"p"}, + Usage: "Runtime platform", + Value: "linux/amd64", + EnvVars: []string{"PLATFORM"}, + }, + }, + Commands: []*cli.Command{ + { + Name: "image", + Aliases: []string{"i"}, + Usage: "Creates container image", + Action: func(cCtx *cli.Context) error { + pipeline := newPipeline(ctx, cCtx, dag) + pipeline.image().publish() + return nil + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} + +type Pipeline struct { + ctx context.Context + dag *dagger.Client + debug bool + nocache bool + platform string + workspace *dagger.Container +} + +func newPipeline(ctx context.Context, cCtx *cli.Context, dag *dagger.Client) *Pipeline { + platform := cCtx.String("platform") + + workspace := dag.Container(dagger.ContainerOpts{ + Platform: dagger.Platform(platform), + }) + + return &Pipeline{ + ctx: ctx, + platform: platform, + debug: cCtx.Bool("debug"), + nocache: cCtx.Bool("nocache"), + dag: dag, + workspace: workspace, + } +} + +func (p *Pipeline) ok() *Pipeline { + var err error + p.workspace, err = p.workspace.Sync(p.ctx) + if err != nil { + panic(err) + } + return p +} + +func (p *Pipeline) image() *Pipeline { + p.workspace = p.workspace.Pipeline("container image"). + From(fmt.Sprintf("ruby:%s", currentToolVersions().Ruby())). + WithExec([]string{"ruby", "--version"}) + + if p.nocache { + p.workspace = p.workspace.WithEnvVariable("CACHE_BUSTED_AT", time.Now().String()) + } + + appSrc := p.dag.Host().Directory(".", dagger.HostDirectoryOpts{ + Include: []string{ + "images", + "lib", + "styles", + "views", + "Gemfile", + "Gemfile.lock", + "LICENSE", + "README.md", + "Rakefile", + }}) + + p.workspace = p.workspace. + WithDirectory("/app", appSrc). + WithWorkdir("/app"). + WithExec([]string{"bundle", "install", "--frozen"}). + WithExec([]string{"rake", "-T"}). + WithEntrypoint([]string{"/bin/bash"}). + WithDefaultArgs() + + if p.debug { + _, err := p.workspace.Pipeline("export tmp/image.tar"). + Export(p.ctx, "tmp/image.tar") + if err != nil { + panic(err) + } + } + + return p.ok() +} + +func (p *Pipeline) publish() *Pipeline { + ghcrUsername := os.Getenv("GHCR_USERNAME") + if ghcrUsername == "" { + fmt.Println("šŸ‘® Skip publishing") + fmt.Println("šŸ‘® GHCR_USERNAME env var is required") + return p + } + registryUsername := ghcrUsername + + ghcrPassword := os.Getenv("GHCR_PASSWORD") + if ghcrPassword == "" { + fmt.Println("šŸ‘® Skip publishing") + fmt.Println("šŸ‘® GHCR_PASSWORD env var is required") + return p + } + registryPassword := p.dag.SetSecret("password", ghcrPassword) + + gitSHA := os.Getenv("GITHUB_SHA") + if gitSHA == "" { + gitSHA = "dev" + } + + imageURL := fmt.Sprintf("ghcr.io/%s/nightly:%s", ghcrUsername, gitSHA) + + var err error + _, err = p.workspace.Pipeline(fmt.Sprintf("publish %s", imageURL)). + WithRegistryAuth("ghcr.io", registryUsername, registryPassword). + Publish(p.ctx, imageURL) + if err != nil { + panic(err) + } + + fmt.Printf("\nāœ… Published as https://%s\n", imageURL) + + return p +} + +type Versions struct { + toolVersions map[string]string +} + +// https://www.ruby-lang.org/en/downloads/releases/ || asdf list all ruby +func (v *Versions) Ruby() string { + return v.toolVersions["ruby"] +} + +func currentToolVersions() *Versions { + return &Versions{ + toolVersions: toolVersions(), + } +} + +func toolVersions() map[string]string { + wd, err := os.Getwd() + if err != nil { + panic(err) + } + versions, err := os.Open(filepath.Join(wd, ".tool-versions")) + if err != nil { + panic(err) + } + toolVersions := make(map[string]string) + scanner := bufio.NewScanner(versions) + for scanner.Scan() { + line := scanner.Text() + toolAndVersion := strings.Split(line, " ") + toolVersions[toolAndVersion[0]] = toolAndVersion[1] + } + + return toolVersions +}