diff --git a/.github/workflows/notify-qa.yml.template b/.github/workflows/notify-qa.yml.template index 6556984e6..7c05c2063 100644 --- a/.github/workflows/notify-qa.yml.template +++ b/.github/workflows/notify-qa.yml.template @@ -2,6 +2,16 @@ name: Notify tutorials-qa on: push: + # -Contribution repos are a MIXTURE of default branches: most default to + # `main`, but Tutorials-Contribution still defaults to `master`. List BOTH + # so this fires ONLY on the repo's default branch — and NEVER on feature + # branches. Without this filter a push to any branch (e.g. the devtoberfest + # validation-tutorial bot's `validation-tutorial/*` branches) dispatches a + # QA rebuild for a slug that exists only on that branch; QA discovery reads + # the default branch, so the slug is a phantom and the rebuild hard-fails + # "unknown slug in filter" (tutorials-ims#2097). Mirrors the branch filter + # in docs/authors/tutorial-repo-dispatch.yml (the PROD notify template). + branches: [master, main] paths: - 'tutorials/**' @@ -17,8 +27,15 @@ jobs: changed=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} \ | awk -F/ '/^tutorials\//{print $2}' | sort -u) count=$(echo "$changed" | wc -l) + # Only emit a slug when EXACTLY one tutorial changed AND it matches the + # strict slug charset (whole-value POSIX case match); anything else → + # empty slug → rebuild-content-qa.yml runs a full rebuild. Mirrors + # docs/authors/tutorial-repo-dispatch.yml. if [ "$count" = "1" ] && [ -n "$changed" ]; then - echo "slug=$changed" >> "$GITHUB_OUTPUT" + case "$changed" in + *[!a-z0-9-]* | -* ) echo "slug=" >> "$GITHUB_OUTPUT" ;; + *) echo "slug=$changed" >> "$GITHUB_OUTPUT" ;; + esac else echo "slug=" >> "$GITHUB_OUTPUT" fi diff --git a/test/unit/install-notify-workflows.test.ts b/test/unit/install-notify-workflows.test.ts index 84a98228c..7b234aed9 100644 --- a/test/unit/install-notify-workflows.test.ts +++ b/test/unit/install-notify-workflows.test.ts @@ -392,3 +392,30 @@ describe('PROD notify template branch trigger (regression guard)', () => { expect(template).not.toMatch(/branches:\s*\[\s*main\s*\]/) }) }) + +describe('QA notify template branch trigger (regression guard)', () => { + // -Contribution repos are ALSO a mixture of `main`- and `master`-default + // branches (Tutorials-Contribution defaults to `master`). The QA template + // MUST scope the push trigger to the default branch(es) — WITHOUT a branches: + // filter it fires on EVERY branch push (e.g. the devtoberfest validation- + // tutorial bot's `validation-tutorial/*` branches), dispatching a QA rebuild + // for a slug that exists only on that branch; QA discovery reads the default + // branch, so the slug is a phantom and the rebuild hard-fails "unknown slug + // in filter" (tutorials-ims#2097). Keep this in parity with the PROD guard. + const template = readFileSync( + join(__dirname, '..', '..', '.github', 'workflows', 'notify-qa.yml.template'), + 'utf8', + ) + + it('fires on both master and main', () => { + expect(template).toMatch(/branches:\s*\[\s*master\s*,\s*main\s*\]/) + }) + + it('does not ship a main-only trigger', () => { + expect(template).not.toMatch(/branches:\s*\[\s*main\s*\]/) + }) + + it('ships a branches filter (never an unfiltered push trigger)', () => { + expect(template).toMatch(/branches:/) + }) +})