So today we have a small example of how, sometimes, properties of separate systems (and us humans are a system for this post) interconnect into making an odd behaviour happen.
So, let’s start with what we are trying to achieve: programmatically creating and triggering an Azure Devops Release Pipeline. Luckily for us, Microsoft provides a Python SDK that we can use to achieve this.
Below is an example of code doing exactly that.
def __create_release(release_client, release_definition_id, project_name): metadata = ReleaseStartMetadata() metadata.definition_id = release_definition_id return release_client.create_release(metadata, project_name) def __trigger_release(release_client, project_name, release, full_stage_name): stage = __get_stage(full_stage_name, release) stage.status = "inProgress" stage.variables = None return release_client.update_release_environment( stage, project_name, stage.release.id, stage.id )
ReleaseStartMetada is one of the objects provided by the SDK.
create_release does exactly as advertise on the tin for a release pipeline. It returns an object of type
__get_stage is just a function that looks for a specific named stage on the release object. We update that stage to be “inProgress” to start the process. And then we update the release. There is a line there that solves the issue we found:
stage.variables = None
The API behaviour
When you crate a release, the object returned has all the information about it … including the variables that you have setup on the release pipeline. And as you can gather from what you see, those variables were originally passed back again on the
update_release_environment. Now if you get the variables and send them back again, where is the issue?
The issue resides on the behaviour around secret variables. When you get the data back on the
Release object after calling
create_release, and you get all variables, secrets variables end with a value of
None (because they are secret, of course). So when you pass the object back the secrets gets overriden with that value (
None). My colleague Chris Bimson, did suggest that probably a better option would have been to send an encrypted version of the secret variable, so even if you overrode the values, it would still have worked correctly.
Well, the above is not completely true: if the secrets are not marked to be overriden, the call to
update_release_environment actually fails, but because I had not yet set the logs properly, I made one of those errors in assuming that the error was on the
create_release (you know, assume makes an ass of me and me). So, I changed the properties of the variables to be able to be rewritten. Pffffft to me.
Remember, logs are important.
Even if is a small cli application, well setup log are your friend (of course, too much logging risk you being swamp in non-useful data).
This is the last part of equation.
create_release returns a
Release object, but
update_release_environment actually takes a
ReleaseEnvironmentUpdateMetadata. As it happens, the member variables used for
update_release_environment are present on a
Release object, so ducktyping comes into effect. Had we been forced by a static type system to change the object, probably we will have realized exactly the issue earlier. I have nothing against duck typing (in fact, I find it quite powerful), and I like dynamic languages in some scenarios (like the one I am in), although there is always the irony of a dynamic language that has the sentence
explicit is better than implicit on their official Zen
Finally, we need to talk about that
stage.variables = None line being a solution to our issue. The behaviour of the API is that if you don’t pass any variable, it uses the default versions for the release. No overriding happens.
The best part of all of it was that, once we put the logging in place and we saw the exact place where it was failing, we quickly created an hypothesis of what was happening and were able to test it immediately.