Stuck Azure pipeline cache

Daniel Lazar
3 min readJul 14, 2024

--

For running some checks and our tests we use Azure in my team, and for better running times we use caching at quite a few places. But sometimes with these caches, we had some trouble.

Let’s say a PR included a new package, and in the meantime, I worked on a different PR, as soon as the PR that included the package was merged and I rebased to the latest master branch, my pipeline suddenly just broke. Why? Well, because our node_modules had a caching step, and it grabbed the wrong cache. It was the one that hadn’t included the new package yet, and other checks were failing because e.g. a type changed, and the linter was complaining. The caching of the dependencies didn’t have anything special, we used what Microsoft recommended in their docs:

- task: Cache@2
inputs:
key: '"yarn" | "$(Agent.OS)" | yarn.lock'
restoreKeys: |
"yarn" | "$(Agent.OS)"
"yarn"
path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages

In these cases, we didn’t have many options:

  1. Recreate PR (meh…)
  2. Wait 7 days so the cache expires (meh…)
  3. Use Microsoft's recommendation (*spoiler*: meh…)

The first two options aren’t great, so what do they recommend? Well, the docs says, we should change this:

key: 'yarn | "$(Agent.OS)" | yarn.lock'

to this:

key: 'version2 | yarn | "$(Agent.OS)" | yarn.lock'

Is it nice? Not really. Is it working? Yes, but still…

After a few times, this issue came around, we tried to think how we could solve this issue. For instance, we could extend what the key has with another variable. Yeah sure, sounds good, but which one? Didn’t find the best for this one.

So, I was wondering, what if somehow we can check the PR’s details, and have something we can go with? But how? Well, the answer is pretty simple: Azure CLI. By default, we can access it on our pipeline without any configuration, and get many details of the PR. One thing is needed which is authentication, but it’s really simple to do. Based on the docs, we do something like this:

- bash: |
# your command will be here later...
displayName: 'azure cli auth example'
env:
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)

and we’re good to go.

First, I was thinking we’ll have a unique tag on our PR which could give us a condition for checking the cache. Yes, that sounds simple enough, right? Yeah, sure, you can access a PR’s details via the az repos pr show --id <pr_id> command, BUT it has a little problem. You can set any tags on your PR however you want, the command’s response will always say "labels": null,. So that’s not too helpful, right? It has a GitHub issue here btw, and based on the answers, they don’t consider this as a bug, but a new feature request. ¯\_(ツ)_/¯

I’ve figured out, and also mentioned in the comments, that if you do az repos pr list -t <target_branch> -s <source_branchh>the response will include the tags, but it will be in an array. If you only need the tags, then you can add the following to your command --query [0].labels and that’s it. If we only need to check if a given tag is provided, then we can modify our query to --query 'contains(not_null([0].labels, [])[].name, `YOUR_TAG`)' and this will return true or false.

With all these, we can set a variable and have a condition in the next steps. For this, an example could be something like this:

- bash: |
SKIP=az repos pr list -t $(System.PullRequest.TargetBranch) -s $(System.PullRequest.SourceBranch) --query 'contains(not_null([0].labels, [])[].name, `SKIP`)'
echo "##vso[task.setvariable variable=SKIP;]$SKIP"
- task: Cache@2
condition: eq(variables['SKIP'], 'false')
inputs:
key: '"yarn" | "$(Agent.OS)" | yarn.lock'
restoreKeys: |
"yarn" | "$(Agent.OS)"
"yarn"
path: $(YARN_CACHE_FOLDER)
displayName: Cache Yarn packages

You can find here the used System variables and the variable set syntax here (which is kinda weird). If needed you can have some extra conditions that you can handle with the or() and and() functions. Plus, you could also use a bit more bash variables so the whole thing is more readable.

We figured all this out recently, there are still some things that we could make better in our pipeline, and for sure this is only a workaround, but so far it works as a charm. So, what do you think about it? If you have any other ideas, or if we missed something let me know in the comments.

PS.: Another way we could handle this is to have azure-cli make an API call with az rest, but this seemed a bit less overkill (maybe).

--

--

Daniel Lazar

I'm a Frontend Engineer, working with Angular, but also like to go into other technologies.