So the callGraph script is neat, but nobody can be expected to have in-depth knowledge of every language they’re attempting to address at first blush. The initial graphs this produced for me were limited, and didn’t list all of the function calls. I found that the regular expressions callGraph uses to recognize function definitions can be extended with optional non-reference generating atoms. In this case, it was missing an understanding of javascript async functions, so I did a quick fork and pull request to insert
...(?:async\s+)?...
After this addition on my local fork, callGraph now successfully recognizes async function definitions, of which there are many in this github actions workflow. callGraph tries to match up specific function definitions and function calls, and without being able to recognize both a definition and a call, it will fail to illustrate that connection. I’m certainly not going on some quest to extend it for all of the myriad ways in which javascript allows functions to be defined or called, that would be the way of madness, and far outside the scope of this project. I just need it ‘good enough’
Having extended it, I now have a more complete call graph:
With the -verbose
flag, callGraph also lists the externally referenced
files, such as modules. It assumes, however, that such files will end in a file
extension matching the passed -language
argument. So, it sees and tells me
about the references to the various nuxt.js and etc files, but because those
are merely read from in order to populate a blank configuration file, and have
no functions associated with them, they don’t show up on the call graph. I know
to look for their references, though.
There are several operations about which I do not care. I got really lost in
the weeds trying to figure that out. The configure-pages action has several
outputs, and as far as I can tell, none of the other actions consume them, per
their respective action.yml
files. The main thing that configure-pages
seems to be concerned with for the purposes of the workflow, is calling out to
the github api and making sure that there exists an endpoint to which the built
site can be pushed and hosted, enabling the functionality if needed by using a
github token, either one auto-generated in the context of the action runner
which it would be if it’s hosted by github, or a personal access token
otherwise. Essentially, none of the action for configuring pages is relevant to
local development, it’s almost all external to the cloud provider, in this case
github pages hosting per their API, or informative.
Otherwise, it’s establishing basic config files for nuxt, next, sveltekit, or
gatsby, none of which are relevant here.
There are several things I learned while I was up to my eyeballs in it, however.
So an action.yml
file defines a bunch of properties which are relevant to
the actions runner. Locally, this is still my installed version of
nektos/act. Critically, this file defines
inputs, if they are required, and their defaults. I passed act
the debug
flags and captured all output to a log, and this debugging output is fairly
thorough, so I’m seeing that there are no inputs of the kinds I think are
missing being passed.
As I was digging into the configure-pages action, I found that one of the first
index.js
calls is to getContext()
, which itself calls
getRequiredVars()
. getContext()
might better be understood as
validateContext(), since that’s its primary function. If one of the required
variables is undefined, it throws an error and the whole operation halts.
But
There’s not a way for that to happen under normal circumstances. I certainly
saw that, and thought, “now wait a second, getRequiredVars()
checks through
several inputs I haven’t supplied, including static_site_generator
, which
is only used if the generator is not jekyll. No default value is specified,
why isn’t this failing?”
My answer was actually in the core github actions toolkit. I’ll explain, but I missed the answer the first readthrough.
My first thought was that in action.yml
, the actions runner is configured
to use the boolean parameter required
if a default
is not set. So, I
searched for “input default when required false”, and found several results.
Critically, a stackoverflow question, github issues, and some github discussion, pertaining to validating that required input is actually passed in.
This only gave me partial insight. For one, that issue, I think, was moved from the actions/toolkit/core repository to the actions/runner repo, where it now lives. Per the context clues of the discussion, their saying that it’s out of scope for the toolkit.
In any event, they pointed to the getInput()
function from the core repository. If you are paying attention, you’ll see the
answer right away.
Surely, I thought, since this is a runner issue, as they say, I should check the runner source code. The runner is evidently written in C#. Although I’ve not written C#, honestly, imperative-OOP language A is imperative-OOP language A. This is not crammed with weird language idioms, it’s fairly straightforward.
Through some hunting around with github’s search, I found the function
call that actually processes the
default value for an input. As my hypothesis as yet unchanged goes, if the
actions runner is silently inserting the value of the required
parameter
when a default is not found, this is where it will be.
It’s not, but it does refer to a unique token I can search for,
input-default-context
. That leads to a json schema
definition.
Damn. The action runner pulls the input default from the default
parameter,
and nowhere else. Well, that’s the github action runner source code, what
about act
source code?
C# is pretty conventional if you know OOP and imperative code. It’s
fundamentally similar to java, for instance. Go is completely new to me, so it
took some amount of reading before I was really grasping what was going on. The
callGraph for act
is larger than I feel comfortable putting on a freely
hosted site. It’s huge.
Between reading through the callGraph and some targeted searching, I’m
reasonably confident that I understand how act
processes inputs from
action.yml as well. My understanding is as follows:
setupActionInputs()
gets a function for
how to step through an action.yml file from the appropriate model sourceaction
with the contents of the actions file
accordingly, including several input keys by their ids.Walking through the model, it also defines the default
input based on the associated default
parameter in action.yml
At this point I’ve got some understanding that, in fact, neither act
, nor
github actions does any kind of sneaky backfilling of the defaults. They both
read only from the default value for the input. act
may or may not
actually respect YAML formatted variables, since I only saw it interpolating
from the default parameter, but that’s more than likely me just not
understanding go that well. So what’s going on? Why, when I’m not giving any
input, nor is the action.yml defining a default, is getContext()
not
throwing errors at undefined variables?
Back in github actions core, my eyes skipped right
over what’s going on. I correctly took in information about checking if the
input has the required
parameter true, and if it has no input then throwing
an error. What I missed was the ||
condition. If there is no environment
variable matching the one named, such as INPUT_STATIC_SITE_GENERATOR, getInput
populates the local variable with an empty string!
An empty string, you will notice, is not undefined. D’oh.
However, an empty string does count as false-y for javascript, which is why
when index.js/main() checks if (staticSiteGenerator)
in a single
conditional later, it fails and moves past that block. js is an abomination,
and there’s just so much implicit behavior going on here, driven by language
idioms.