@@ -68,19 +68,44 @@ const allUsedActions = chain(workflows)
6868
6969const scheduledWorkflows = workflows . filter ( ( { data } ) => data . on . schedule )
7070
71- const alertWorkflows = workflows
72- // Only include jobs running on docs-internal
73- . filter ( ( { data } ) =>
74- Object . values ( data . jobs )
75- . map ( ( job ) => job . if )
76- . toString ( )
77- . includes ( 'docs-internal' ) ,
78- )
79- // Require slack alerts on workflows that aren't actively watched at time of run
80- . filter ( ( { data } ) => data . on . schedule || data . on . push || data . on . issues || data . on . issue_comment )
81- // Not including
82- // - premerge workflows: pull_request, pull_request_target, pull_request_review, merge_group
83- // - adhoc workflows: workflow_dispatch, workflow_run, workflow_call, repository_dispatch
71+ // Triggers where a workflow runs without a human actively watching and
72+ // therefore needs explicit failure reporting (Slack + issue). Attended
73+ // triggers (pull_request*, workflow_dispatch, workflow_call, merge_group)
74+ // are intentionally excluded: the person who triggered the run sees the
75+ // result directly.
76+ //
77+ // `issues` and `issue_comment` are only considered unattended for jobs
78+ // running in docs-internal itself. When a job is scoped to the public
79+ // github/docs fork via `if: github.repository == 'github/docs'`, those
80+ // triggers fire from external reporters/commenters, and the issue or
81+ // comment itself is the natural failure surface — piling on automated
82+ // alert-issues there is duplicative and noisy.
83+ const ALWAYS_UNATTENDED_TRIGGERS = [ 'schedule' , 'workflow_run' , 'repository_dispatch' , 'push' ]
84+ const DOCS_INTERNAL_ONLY_UNATTENDED_TRIGGERS = [ 'issues' , 'issue_comment' ]
85+
86+ function jobIsPublicDocsScoped ( job : WorkflowJob ) : boolean {
87+ return typeof job . if === 'string' && / g i t h u b \. r e p o s i t o r y \s * = = \s * [ ' " ] g i t h u b \/ d o c s [ ' " ] / . test ( job . if )
88+ }
89+
90+ function jobRequiresFailureAlerts ( workflow : WorkflowMeta , job : WorkflowJob ) : boolean {
91+ const triggers = workflow . data . on || { }
92+ if ( ALWAYS_UNATTENDED_TRIGGERS . some ( ( t ) => ( triggers as Record < string , unknown > ) [ t ] ) ) {
93+ return true
94+ }
95+ if (
96+ ! jobIsPublicDocsScoped ( job ) &&
97+ DOCS_INTERNAL_ONLY_UNATTENDED_TRIGGERS . some ( ( t ) => ( triggers as Record < string , unknown > ) [ t ] )
98+ ) {
99+ return true
100+ }
101+ return false
102+ }
103+
104+ // Workflows where at least one job requires failure alerts — used to drive
105+ // the parameterised tests below. Per-job filtering happens inside each test.
106+ const alertWorkflows = workflows . filter ( ( { data } ) =>
107+ Object . values ( data . jobs ) . some ( ( job ) => job . steps ) ,
108+ )
84109// to generate list, console.log(new Set(workflows.map(({ data }) => Object.keys(data.on)).flat()))
85110
86111const dailyWorkflows = scheduledWorkflows . filter ( ( { data } ) =>
@@ -151,23 +176,22 @@ describe('GitHub Actions workflows', () => {
151176 }
152177 } )
153178
154- test . each ( alertWorkflows ) (
155- 'scheduled workflows slack alert on fail $filename' ,
156- ( { filename, data } ) => {
157- for ( const [ name , job ] of Object . entries ( data . jobs ) ) {
158- if (
159- ! job . steps . find ( ( step : WorkflowStep ) => step . uses === './.github/actions/slack-alert' )
160- ) {
161- throw new Error ( `Job ${ filename } # ${ name } missing slack alert on fail` )
162- }
179+ test . each ( alertWorkflows ) ( 'unattended workflows slack alert on fail $filename' , ( workflow ) => {
180+ const { filename, data } = workflow
181+ for ( const [ name , job ] of Object . entries ( data . jobs ) ) {
182+ if ( ! jobRequiresFailureAlerts ( workflow , job ) ) continue
183+ if ( ! job . steps . find ( ( step : WorkflowStep ) => step . uses === './.github/actions/slack-alert' ) ) {
184+ throw new Error ( `Job ${ filename } # ${ name } missing slack alert on fail` )
163185 }
164- } ,
165- )
186+ }
187+ } )
166188
167189 test . each ( alertWorkflows ) (
168- 'scheduled workflows create failure issue on fail $filename' ,
169- ( { filename, data } ) => {
190+ 'unattended workflows create failure issue on fail $filename' ,
191+ ( workflow ) => {
192+ const { filename, data } = workflow
170193 for ( const [ name , job ] of Object . entries ( data . jobs ) ) {
194+ if ( ! jobRequiresFailureAlerts ( workflow , job ) ) continue
171195 if (
172196 ! job . steps . find (
173197 ( step : WorkflowStep ) => step . uses === './.github/actions/create-workflow-failure-issue' ,
@@ -181,8 +205,10 @@ describe('GitHub Actions workflows', () => {
181205
182206 test . each ( alertWorkflows ) (
183207 'performs a checkout before calling composite action $filename' ,
184- ( { filename, data } ) => {
208+ ( workflow ) => {
209+ const { filename, data } = workflow
185210 for ( const [ name , job ] of Object . entries ( data . jobs ) ) {
211+ if ( ! jobRequiresFailureAlerts ( workflow , job ) ) continue
186212 if ( ! job . steps . find ( ( step : WorkflowStep ) => checkoutRegexp . test ( step . uses || '' ) ) ) {
187213 throw new Error (
188214 `Job ${ filename } # ${ name } missing a checkout before calling the composite action` ,
0 commit comments