rules and the older only/except keywords are mutually exclusive within a single job. Using both produces a pipeline that fails to create with jobs:NAME config key may not be used with rules: only at lint time, before anything runs.
The pair can coexist across a pipeline, so one job may use only while another uses rules, but a job that inherits only from a template through extends and then adds its own rules hits the conflict. This is the common way it appears: the conflict is invisible in the job's own definition and lives in the extended base.
GitLab documents rules as the replacement and only/except as no longer being actively developed. Run gitlab-ci lint through the project's CI Lint page or the API before pushing, and when converting a template, convert every job that extends it in the same change. The direct translation of only: [main] is rules: - if: $CI_COMMIT_BRANCH == "main".