That is the "strategy" part of the workflow, not the "tasks" part. My comment was about the latter. ("and make your GH Actions run `make` in a script task.")
>restart failed portions without retrying the whole CI, run certain sections with conditions like which branch you are on (different tasks when commit to master vs feature branch)
So can a shell script.
Yes now try waiting for all parallel tasks, and error out if at least one errors. And try separating their output so that they don't get interleaved into a big mess. And try by default hiding the outputs of commands that succeeded, only showing those that failed, except when the user explicitly asks for it.
Your "simple" shell script now suddenly isn't so simple anymore.
all: variant-1 variant-2
@echo done with all
variant-%:
@echo starting $@
@sleep 1
@echo done $@
Use `make -j2 --output-sync=target`. brew install make
gmake -j2 --output-sync=target
on macOS.`wait`
>and error out if at least one errors.
`wait` propagates the exit status of the waited task. `set -e` triggers the script to exit on any command failure.
>And try separating their output so that they don't get interleaved into a big mess.
`| while read -r line; do echo "task X: $line"; done`
Learn the tools instead of being so confident that it can't be done or that it's complicated.
The CI runners aren’t multi core VMs I believe so you can’t just use standard shell utilities, you have to indicate to the CI system you want to run multiple tasks.
I also suggest Bazel as a consideration alongside Make. With Bazel, you get two advantages over Make:
1. It is easier to ensure that what GitHub Actions runs and builds is the same as what you have locally, since Bazel can fetch toolchains as part of the build process
2. Using a Bazel remote cache, you do not have to repeat work if the build fails halfway and you need to make some changes before running it again.
Now my makefiles in addition to the usual "make" and "make test" also support "make prerequisites" to prepare a build environment by installing everything necessary and "make ci" to run everything that CI should check. With actual implementation being scripts placed under "scripts/ci".
The scripts do provide some goodies when they are run by GitHub Actions – like folding the build logs or installing dependencies differently – but these targets also work on the developer machine.
If it’s unbearable due to circumstances out of your control, there’s nothing wrong with adding some actions/cache steps to .github/workflows – this goes around the build: fetch previous cache before, update the cache after if needed.
The build is still reproducible outside of GitHub Actions, but a pinch of magic salt makes it go faster sometimes without being an essential part of the build pipeline married to GitHub.
If you need to install a whole host of mostly static dependencies, GitHub Actions support running steps in arbitrary Docker container. Prepare an image beforehand, it can be cached too, now you have a predictable environment. (The only downside is that it doesn’t work on macOS and Windows.)
Actually I use a similar workflow for some of the other projects that I have to work with, keeping everything CI agnostic. I incrementally add various types of dependencies to the container images where the application will be built.
For example:
1. common base image (e.g. Debian, Ubuntu, or something like Alpine, or maybe RPM based ones)
2. previous + common tools (optional, if you want to include curl or other tools in the container for debugging or testing stuff quickly)
3. previous + project runtime (depending on tech stack, for example OpenJDK for Java projects)
4. previous + development tools (depending on the tech stack, typically for pulling in dependencies, like Maven, or npm, or whatever)
5. previous + project dependencies (if the project is large and the dependencies change rarely, you can install them once here and the changing 5% or so later)
6. previous + project build (including things like running tests, typically multi-stage with the build and tests, and built app handled separately)
Compared to the more "common" way to do things, step #5 probably jumps out the most here, I do a pass of installing all of the dependencies, say, every morning, or hourly in the background, so that later when the project is built the CI can just go: "Hmm, it seems like 95% of the things I need here are already present, I'll just pull the remaining packages (if any)." Clean installs only need to be done when packages are removed, which is also reasonably easy to do.Though the benefits of this aren't quite as staggering, if you use a self-hosted package repository like Sonatype Nexus, which can cache any dependencies that you've used previously and make everything faster on the network I/O side. This only doesn't hold true when actually installing the packages takes up the majority of the time (e.g. compiling native code), in which case the above is still very useful.
So, an example of how the stages might look, is as follows:
Builder: Ubuntu + tools (optional) + OpenJDK + Maven + project dependencies + project build (and run tests)
Runner: Ubuntu + tools (optional) + OpenJDK + built project from last image (using COPY with --from, typically .jar file or app directory)
Of course, things are less comfortable when you don't have all of your app's dependencies packaged statically but need them "on the system" instead, like Python packages or Ruby Gems, but then your builder and runner will simply look more alike.For my own personal stuff I also use a slightly simplified version of this, about which I wrote on my blog here, the drawbacks included: https://blog.kronis.dev/articles/using-ubuntu-as-the-base-fo...
I think the method you describe is still absolutely how it should be, but this types of interactions are why there’s gravity in the other direction.
Apart from triggers and environment set up none of those things have to be unique.
I often push complex CI logic in YAML into code where it is more easily debugged and I dont have to scratch my head to figure out how to use conditionals. Sending slack messages should always be in code IMHO.
Heck make can make all this much better by just prepending each output line with some colored prefix and timestamp. But make hasn't changed in 30 years and likely won't change.
People are proud that it "solves" things since 1976. Yes if your requirements never changed since 1976. I'm not holding by breath that it will deliver basic usability-enhancing features that one can reasonably expect nowadays.
A working example of this is at: https://github.com/nickjj/docker-flask-example/blob/912388f3...
Those ./run ci:XXX commands are in: https://github.com/nickjj/docker-flask-example/blob/912388f3...
I like it because if CI ever happens to be down I can still run that shell script locally.
Yes, because proper use of the tool cache (and other caches) significantly speed up GitHub Actions builds.
Look, I'm all for putting logic in Makefiles instead of in YAML. But not at the expense of slower (and therefore more expensive) builds!
* GNU make is sometimes unavailable
* syntax is an acquired taste
* not everything fits in a rule body
Imho, these are far outweighed by the flexibility, portability, and "least surprise" convention embodied in a Makefile.
Do you really need that GH Action for pulling Docker images / installing $language_compiler / creating cloud resources ? A `docker` / `curl` / `sudo apt-get install` invocation in a Makefile / script needs to be written once and is the same in CI as on your dev machines. The Action is GH-specific, requires you to look it up, requires you to learn the DSL for invoking it, requires you to keep it up-to-date, requires you to worry about it getting hacked and stealing your secrets, ...
A Makefile already supports dependencies between targets. A shell script is a simple DSL for executing processes, piping the output of one to another, and detecting and acting on failure of subcommands. How much YAML spaghetti do you need to write to do those same things in a workflow file?