Splitting cache restore and cache save? (Migrating from GitHub Actions)

Below is a sample GitHub Action config, pulled from GitHub - jeremyjh/dialyxir: Mix tasks to simplify use of Dialyzer in Elixir projects., but it’s very close to what I’m also using.

I’m trying to figure out an equivalent GitLab CI setup. It seems that GitLab CI’s cache operates similar to GitHub actions/cache@v2 Action in that it will not save the cache if any step of a given job fails. I’d like to split that up in GitLab CI into two separate steps like so:

1. Restore cache of static analysis artifacts if it exists
2. Incrementally generate updated static analysis artifacts based on cached ones
3. Cache updated static analysis artifacts
4. Perform static analysis check

Is there a way I could accomplish that? Any and all tips are appreciated!

The GitHub Action config:

# github-actions-dialyzer.yml
# ...
steps:
  - uses: actions/checkout@v2
  - name: Set up Elixir
    id: beam
    uses: erlef/setup-beam@v1
    with:
      elixir-version: "1.12.3" # Define the elixir version
      otp-version: "24.1" # Define the OTP version

   # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones
   # Cache key based on Elixir & Erlang version (also useful when running in matrix)
  - name: Restore PLT cache
    uses: actions/cache/restore@v3
    id: plt_cache
    with:
      key: |
        ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt
      restore-keys: |
        ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt
      path: |
        priv/plts

  # Create PLTs if no cache was found
  - name: Create PLTs
    if: steps.plt_cache.outputs.cache-hit != 'true'
    run: mix dialyzer --plt
     
  # By default, the GitHub Cache action will only save the cache if all steps in the job succeed,
  # so we separate the cache restore and save steps in case running dialyzer fails.
  - name: Save PLT cache
    uses: actions/cache/save@v3
    if: steps.plt_cache.outputs.cache-hit != 'true'
    id: plt_cache_save
    with:
      key: |
        ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt
      path: |
        priv/plts

  - name: Run dialyzer
    run: mix dialyzer --format github
# ...

I ended up solving this myself after a couple hours of trial-and-error. Here is a simplified version of my working solution: Add GitLab CI example with dialyzer cache, refactor sample CI configs into new docs directory by Nezteb · Pull Request #490 · jeremyjh/dialyxir · GitHub

It might not be perfect, but it works significantly better than before!

Here is the full yaml just in case:

# Some of the duplication can be reduced with YAML anchors:
# https://docs.gitlab.com/ee/ci/yaml/yaml_optimization.html

image: elixir:1.14

stages:
  - compile
  - check-elixir-types

# You'll want to cache based on your Erlang/Elixir version.

# The example jobs below uses asdf's config file as the cache key:
# https://asdf-vm.com/manage/configuration.html

# An example build job with cache, to prevent dialyzer from needing to compile your project first
build-dev:
  stage: compile
  cache:
    - key:
      files:
        - mix.lock
        - .tool-versions
      paths:
        - deps/
        - _build/dev
      policy: pull-push
  script:
    - mix do deps.get, compile

# The main difference between the following jobs is their cache policy:
# https://docs.gitlab.com/ee/ci/yaml/index.html#cachepolicy

dialyzer-plt:
  stage: check-elixir-types
  needs:
    - build-dev
  cache:
    - key:
      files:
        - .tool-versions
      paths:
        - priv/plts
      # Pull cache at start, push updated cache after completion
      policy: pull-push
  script:
    - mix dialyzer --plt

dialyzer-check:
  stage: check-elixir-types
  needs:
    - dialyzer-plt
  cache:
    - key:
      files:
        - .tool-versions
      paths:
        - priv/plts
      # Pull cache at start, don't push cache after completion
      policy: pull
  script:
    - mix dialyzer --format short

# ...