diff --git a/.github/workflows/publish-module-api-doc.yaml b/.github/workflows/publish-module-api-doc.yaml
index 1b52a4e5ce973a95ffcb52c540da3660caaca3b9..22604b8171a8f342343aff734981a9b32f8d84e8 100644
--- a/.github/workflows/publish-module-api-doc.yaml
+++ b/.github/workflows/publish-module-api-doc.yaml
@@ -7,6 +7,26 @@ on:
     types: [opened, reopened, synchronize, labeled]
   workflow_dispatch:
 
+concurrency:
+  # Update of a developer branch cancels the previously scheduled workflow
+  # run for this branch. However, the 'master' branch, release branch (1.10,
+  # 2.8, etc.), and tag workflow runs are never canceled.
+  #
+  # We use a trick here: define the concurrency group as 'workflow run ID' +
+  # 'workflow run attempt' because it is a unique combination for any run.
+  # So it effectively discards grouping.
+  #
+  # Important: we cannot use `github.sha` as a unique identifier because
+  # pushing a tag may cancel a run that works on a branch push event.
+  group: ${{ (
+    github.ref == 'refs/heads/master' ||
+    github.ref == 'refs/heads/1.10' ||
+    startsWith(github.ref, 'refs/heads/2.') ||
+    startsWith(github.ref, 'refs/tags/')) &&
+    format('{0}-{1}', github.run_id, github.run_attempt) ||
+    format('{0}-{1}', github.workflow, github.ref) }}
+  cancel-in-progress: true
+
 jobs:
   publish-api-doc:
     # Run on push to the branch 'master' or on pull request if the 'full-ci'