├── .gitignore ├── LICENSE ├── README.md ├── cue.mod ├── module.cue └── sums.cue ├── harmony.cue └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | cue.mod/pkg/ 2 | */cue.mod/pkg/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, _Hofstadter 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # harmony 2 | 3 | `harmony` is a framework for 4 | testing a suite of repositories 5 | across versions of dependencies. 6 | Discover errors and regressions 7 | in downstream projects before 8 | releasing new code. 9 | `harmony` makes it easy for both developers and users 10 | to setup and register projects for usage in upstream testing. 11 | 12 | Built on [Dagger](https://dagger.io), `harmony` provides 13 | 14 | - simple setup for developers 15 | - simple registration for user projects 16 | - support for any languages or tools 17 | - consistent environment for testing 18 | - simplified version injection 19 | - easily run specific or all cases 20 | 21 | _Note, while `harmony` uses Dagger, it is not required for downstream projects._ 22 | 23 | #### Examples 24 | 25 | - [harmony-cue](https://github.com/hofstadter-io/harmony-cue) for testing CUE base projects 26 | 27 | ## Harmony Setup 28 | 29 | To setup `harmony`, add the following file to your project. 30 | 31 | ```cue 32 | package main 33 | 34 | import ( 35 | "dagger.io/dagger" 36 | "universe.dagger.io/docker" 37 | 38 | "github.com/hofstadter-io/harmony" 39 | 40 | // import your projects registry 41 | "github.com/username/project/registry" 42 | ) 43 | 44 | // A dagger plan is used as the driver for testing 45 | dagger.#Plan 46 | 47 | // add actions from Harmony 48 | actions: harmony.Harmony 49 | 50 | // project specific actions & configuration 51 | actions: { 52 | 53 | // global version config for this harmony 54 | versions: { 55 | go: "1.18" 56 | } 57 | 58 | // the registry of downstream projects 59 | // typically we put this in a subdir and import it 60 | "registry": registry.Registry 61 | 62 | // the image test cases are run in 63 | // typically parametrized so we can change dependencies or versions 64 | // (you can also build any image you want here) 65 | runner: docker.#Pull & { 66 | source: "index.docker.io/golang:\(versions.go)-alpine" 67 | } 68 | 69 | // where downstream project code is checked out 70 | workdir: "/work" 71 | } 72 | ``` 73 | 74 | Run registration cases or a single case with dagger: `dagger do [case]`. 75 | Any cases found will be run in parallel. 76 | 77 | Use `./run.py` to run the full suite of registrations and cases sequentially, 78 | or as a convenient way to set dependency versions. 79 | 80 | #### Registry 81 | 82 | You will typically want to provide a subdirectory 83 | for registered projects. You can also provide 84 | short codes to simplify user project registration further. 85 | 86 | Here we add a `_dagger` short code by 87 | including a `schema.cue` in our `registry/` directory. 88 | 89 | ```cue 90 | package registry 91 | 92 | import ( 93 | "strings" 94 | 95 | "universe.dagger.io/docker" 96 | "github.com/hofstadter-io/harmony" 97 | ) 98 | 99 | // customized Registration schema built on harmony's 100 | Registration: harmony.Registration & { 101 | // add our short codes 102 | cases: [string]: docker.#Run & { 103 | _dagger?: string 104 | if _dagger != _|_ { 105 | command: { 106 | name: "bash" 107 | args: ["-c", _script] 108 | _script: """ 109 | dagger version 110 | dagger project update 111 | dagger do \(_dagger) 112 | """ 113 | } 114 | } 115 | } 116 | } 117 | ``` 118 | 119 | Registrations then use with `cases: foo: { _dagger: "foo bar", workdir: "/work" }` 120 | 121 | 122 | ## Registration Setup 123 | 124 | To add a new registration for user projects, 125 | add a CUE file to the upstream project. 126 | 127 | ```cue 128 | package registry 129 | 130 | // Note the 'Registry: : ...` needs to be unique 131 | Registry: hof: Registration & { 132 | // 133 | remote: "github.com/hofstadter-io/hof" 134 | ref: "main" 135 | 136 | cases: { 137 | // these are docker.#Run from Dagger universe 138 | cli: { 139 | workdir: "/work" 140 | command: { 141 | name: "go" 142 | args: ["build", "./cmd/hof"] 143 | } 144 | flow: { 145 | workdir: "/work" 146 | command: { 147 | name: "go" 148 | args: ["test", "./flow"] 149 | } 150 | } 151 | } 152 | ``` 153 | 154 | ## Base image using dirty code 155 | 156 | As a developer of a project, 157 | you may wish to run harmony 158 | before committing your code. 159 | To do this, we need to 160 | 161 | - mount the local directory into dagger 162 | - build a runner image from the local code 163 | - mount the local directory into the runner (possibly) 164 | 165 | Refer to [harmony-cue](https://github.com/hofstadter-io/harmony-cue) for an example. 166 | Look at how "local" is used: 167 | 168 | - harmony.cue 169 | - testers/image.cue 170 | - run.py 171 | 172 | The essence is to use CUE guards (if statements) 173 | 174 | --- 175 | 176 | `harmony` was inspired by the idea 177 | of creating a [cue-unity](https://github.com/cue-unity/unity) 178 | powered by [Dagger](https://dagger.io). 179 | The goal of `cue-unity` is to collect community projects 180 | and run them against new CUE language changes. 181 | `cue-unity` was itself inspired by some work 182 | Rob Pike did on Go, for the same purpose 183 | of testing downstream projects using Go's stdlib. 184 | 185 | `harmony` is more generalized and 186 | the CUE specific version is [harmony-cue](https://github.com/hofstadter-io/harmony-cue). 187 | -------------------------------------------------------------------------------- /cue.mod/module.cue: -------------------------------------------------------------------------------- 1 | module: "github.com/hofstadter-io/harmony" 2 | cue: "v0.4.3" 3 | 4 | require: { 5 | } 6 | -------------------------------------------------------------------------------- /cue.mod/sums.cue: -------------------------------------------------------------------------------- 1 | sums: { 2 | } 3 | -------------------------------------------------------------------------------- /harmony.cue: -------------------------------------------------------------------------------- 1 | package harmony 2 | 3 | import ( 4 | cuecsv "encoding/csv" 5 | "list" 6 | 7 | "universe.dagger.io/docker" 8 | "universe.dagger.io/git" 9 | ) 10 | 11 | // Schema for project registration 12 | Registration: { 13 | // git repository to clone 14 | remote: string 15 | 16 | // git ref to checkout 17 | ref: string 18 | 19 | // testing cases 20 | cases: [string]: docker.#Run 21 | // often you will provide some short codes 22 | 23 | // versions injected into commands by the driver 24 | versions: [string]: string 25 | // do not fill, but can be referenced 26 | // particularly useful for short codes 27 | 28 | // feel free to add more fields for your needs 29 | } 30 | 31 | // Dagger actions for harmony 32 | Harmony: { 33 | 34 | // version config 35 | versions: [string]: string 36 | 37 | // the registered projects 38 | registry: [string]: Registration 39 | 40 | // image tests are run in 41 | // ofter parameterized by the versions 42 | runner: docker.#Image 43 | 44 | // directory where users' code is cloned 45 | workdir: string 46 | 47 | // extra config 48 | extra: { 49 | // injected into the registrations 50 | reg: {...} 51 | // injected into the docker.#Run, per case 52 | run: {...} 53 | } 54 | 55 | // creates actions for each registration 56 | for name, reg in registry { 57 | (name): { 58 | _reg: reg & { 59 | extra.reg 60 | "versions": versions 61 | } 62 | // clone the remote 63 | clone: git.#Pull & { 64 | remote: _reg.remote 65 | ref: _reg.ref 66 | keepGitDir: true 67 | } 68 | 69 | // run a container for each case 70 | for key, case in _reg.cases { 71 | (key): docker.#Run & { 72 | always: true 73 | 74 | // setup base 75 | let R = runner 76 | input: R 77 | 78 | // embed case 79 | case 80 | 81 | // embed case run extra 82 | extra.run 83 | 84 | // where we put the source repository 85 | mounts: { 86 | work: { 87 | contents: clone.output 88 | dest: workdir 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | // used via 'cue eval -e actions.csv' 97 | // to get action list for use in scripts 98 | // see run.py for an example 99 | csv: cuecsv.Encode(list.FlattenN([ 100 | for name, reg in registry { 101 | [ for key, case in reg.cases { [name, key] } ] 102 | } 103 | ], 1)) 104 | } 105 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # 4 | # this is taken from harmony-cue as an example 5 | # 6 | 7 | import argparse 8 | import csv 9 | import os 10 | import subprocess 11 | 12 | # args and flags to the script 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument('path', nargs='*', help="dagger do action path") 15 | parser.add_argument('--cuepath', help="path to local cue repository") 16 | parser.add_argument('--cue', help="cue version") 17 | parser.add_argument('--dagger', help="dagger version") 18 | parser.add_argument('--go', help="go version") 19 | parser.add_argument('--fmt', help="dagger log format", default="plain") 20 | parser.add_argument('--no-cache', help="disable dagger (buildkit) cache" , action='store_true', default=False) 21 | args = parser.parse_args() 22 | 23 | # get full list from cue 24 | p = os.popen("cue eval -e actions.csv --out text") 25 | out = p.read().strip() 26 | actions = list(csv.reader(out.split("\n"))) 27 | 28 | # start `--with` content 29 | dagger_with = "'actions: { " 30 | 31 | # possibly add CUE path 32 | if args.cuepath is not None: 33 | dagger_with += f"pathToCUE: \"{args.cuepath}\"" 34 | args.cue = "local" 35 | 36 | # build up the injected version CUE code 37 | vers = "" 38 | if args.cue is not None: 39 | if args.cue == "local": 40 | dagger_with += ", " 41 | vers += f'cue: "{args.cue}"' 42 | if args.go is not None: 43 | if vers != "": 44 | vers += ", " 45 | vers += f'go: "{args.go}"' 46 | if args.dagger is not None: 47 | if vers != "": 48 | vers += ", " 49 | vers += f'dagger: "{args.dagger}"' 50 | 51 | if vers != "": 52 | dagger_with += f"versions: {{ {vers} }}" 53 | 54 | dagger_with += "}'" 55 | # done constructing '--with' content 56 | 57 | flags = ["--log-format", args.fmt, "--with", dagger_with] 58 | if args.no_cache: 59 | flags.append("--no-cache") 60 | 61 | for action in actions: 62 | # enable pass through of dagger do args 63 | match = True 64 | for i, p in enumerate(args.path): 65 | if p != action[i]: 66 | match = False 67 | break 68 | 69 | if match: 70 | cmd = ["dagger", "do"] + action + flags 71 | print("Running:", " ".join(cmd)) 72 | subprocess.run(["bash", "-c", " ".join(cmd)], check=True) 73 | --------------------------------------------------------------------------------