Unfortunately this is the expected behavior - tags trigger a separate pipeline, even when they are part of the same push as the commit. You can block this with workflows, but in your case it would prevent the deploy job from running.
You can prevent the same job running in both pipelines with rules, which is now recommended over only/except. It's very flexible when combined with CI/CD vars. I would try the bash regex operator with CI_COMMIT_TAG
:
stages:
- build
- deploy
build:
stage: build
script: //do stuff
rules:
- if: '$CI_COMMIT_TAG =~ "<your_regex>"'
when: never
- when: always
deploy:
stage: deploy
script: //do stuff
rules:
- if: '$CI_COMMIT_TAG =~ "<your_regex>"'
when: always
- when: never
Rules are read top to bottom until a match is found. The catch-all rules at the end ensure that:
build
always runs unless the regex matches
deploy
only runs when the regex matches
This will still create 2 pipelines, but it should prevent the build job from running twice and wasting minutes.