forked from paperswithcode/ai-deadlines
-
Notifications
You must be signed in to change notification settings - Fork 29
1329 lines (1158 loc) · 58.2 KB
/
Copy pathcheck-conference-update.yml
File metadata and controls
1329 lines (1158 loc) · 58.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: Check Conference Update
on:
# Triggered by changedetection.io webhook
repository_dispatch:
types: [conference-change]
# Manual trigger for testing
workflow_dispatch:
inputs:
url:
description: 'Conference website URL to check'
required: true
type: string
conference_name:
description: 'Conference name (e.g., "PyCon US")'
required: true
type: string
skip_triage:
description: 'Skip triage and go straight to full analysis'
required: false
type: boolean
default: false
force_zone:
description: 'Override zone detection (normal/slow/dead)'
required: false
type: string
default: ''
# Bug #3 Fix: Per-conference concurrency to prevent duplicate triggers
# Different conferences can run in parallel, but the same conference serializes
# This prevents race conditions when changedetection.io sends multiple notifications
concurrency:
group: conference-update-${{ github.event.client_payload.watch_uuid || inputs.url || github.run_id }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
issues: write
jobs:
# Stage 0: Determine monitoring zone BEFORE spending money on triage
# This is FREE - just reads YAML and does date math
zone-check:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
zone: ${{ steps.detect.outputs.zone }}
skip: ${{ steps.detect.outputs.skip }}
triage_mode: ${{ steps.detect.outputs.triage_mode }}
url: ${{ steps.vars.outputs.url }}
conference: ${{ steps.vars.outputs.conference }}
diff_b64: ${{ steps.vars.outputs.diff_b64 }}
watch_uuid: ${{ steps.vars.outputs.watch_uuid }}
has_diff: ${{ steps.vars.outputs.has_diff }}
has_pending: ${{ steps.pending.outputs.has_pending }}
# Trigger context from scheduled extension checker
trigger_reason: ${{ steps.vars.outputs.trigger_reason }}
original_cfp_deadline: ${{ steps.vars.outputs.original_cfp_deadline }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Validate payload
id: validate
env:
EVENT_NAME: ${{ github.event_name }}
PAYLOAD_URL: ${{ github.event.client_payload.url }}
PAYLOAD_TITLE: ${{ github.event.client_payload.title }}
PAYLOAD_UUID: ${{ github.event.client_payload.watch_uuid }}
INPUT_URL: ${{ inputs.url }}
INPUT_CONFERENCE: ${{ inputs.conference_name }}
run: |
# Payload validation: Fail fast with clear error messages if required fields are missing
# This helps diagnose issues like expired tokens or malformed webhook payloads
echo "::group::Payload Validation"
echo "Event: $EVENT_NAME"
if [ "$EVENT_NAME" = "repository_dispatch" ]; then
MISSING_FIELDS=""
if [ -z "$PAYLOAD_URL" ]; then
MISSING_FIELDS="${MISSING_FIELDS}url, "
echo "::error title=Missing Field::client_payload.url is required for repository_dispatch"
fi
if [ -z "$PAYLOAD_TITLE" ]; then
MISSING_FIELDS="${MISSING_FIELDS}title, "
echo "::error title=Missing Field::client_payload.title is required for repository_dispatch"
fi
if [ -z "$PAYLOAD_UUID" ]; then
echo "::warning title=Missing Field::client_payload.watch_uuid is missing - concurrency may not work correctly"
fi
if [ -n "$MISSING_FIELDS" ]; then
echo "::endgroup::"
echo ""
echo "## ❌ Payload Validation Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Missing required fields: ${MISSING_FIELDS%, }" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "This usually indicates one of:" >> $GITHUB_STEP_SUMMARY
echo "- The GitHub PAT token used by changedetection.io is expired or has insufficient permissions" >> $GITHUB_STEP_SUMMARY
echo "- The webhook payload is malformed" >> $GITHUB_STEP_SUMMARY
echo "- The repository_dispatch event was triggered with incomplete data" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "✓ All required fields present for repository_dispatch"
else
# workflow_dispatch validation
if [ -z "$INPUT_URL" ]; then
echo "::error title=Missing Input::url input is required for manual trigger"
echo "::endgroup::"
exit 1
fi
if [ -z "$INPUT_CONFERENCE" ]; then
echo "::error title=Missing Input::conference_name input is required for manual trigger"
echo "::endgroup::"
exit 1
fi
echo "✓ All required inputs present for workflow_dispatch"
fi
echo "::endgroup::"
- name: Set up variables
id: vars
env:
# Pass webhook payload via env to prevent injection
PAYLOAD_URL: ${{ github.event.client_payload.url }}
PAYLOAD_TITLE: ${{ github.event.client_payload.title }}
PAYLOAD_DIFF: ${{ toJson(github.event.client_payload.diff) }}
PAYLOAD_UUID: ${{ github.event.client_payload.watch_uuid }}
# Trigger context from scheduled extension checker
PAYLOAD_TRIGGER_REASON: ${{ github.event.client_payload.trigger_reason }}
PAYLOAD_ORIGINAL_CFP: ${{ github.event.client_payload.original_cfp_deadline }}
INPUT_URL: ${{ inputs.url }}
INPUT_CONFERENCE: ${{ inputs.conference_name }}
EVENT_NAME: ${{ github.event_name }}
run: |
# Bug #2 Fix: Use base64 encoding for diff content to prevent heredoc delimiter collision
# The diff can contain arbitrary website content including strings that look like delimiters
if [ "$EVENT_NAME" = "repository_dispatch" ]; then
echo "url=$PAYLOAD_URL" >> $GITHUB_OUTPUT
echo "conference=$PAYLOAD_TITLE" >> $GITHUB_OUTPUT
# Base64 encode the diff to safely pass multi-line content with special characters
DIFF_B64=$(printf '%s' "$PAYLOAD_DIFF" | base64 -w 0)
echo "diff_b64=$DIFF_B64" >> $GITHUB_OUTPUT
echo "watch_uuid=$PAYLOAD_UUID" >> $GITHUB_OUTPUT
echo "has_diff=true" >> $GITHUB_OUTPUT
# Trigger context (empty if from changedetection.io, populated if from scheduled checker)
echo "trigger_reason=${PAYLOAD_TRIGGER_REASON:-website_change}" >> $GITHUB_OUTPUT
echo "original_cfp_deadline=${PAYLOAD_ORIGINAL_CFP:-}" >> $GITHUB_OUTPUT
else
echo "url=$INPUT_URL" >> $GITHUB_OUTPUT
echo "conference=$INPUT_CONFERENCE" >> $GITHUB_OUTPUT
echo "has_diff=false" >> $GITHUB_OUTPUT
echo "watch_uuid=" >> $GITHUB_OUTPUT
echo "diff_b64=" >> $GITHUB_OUTPUT
echo "trigger_reason=manual" >> $GITHUB_OUTPUT
echo "original_cfp_deadline=" >> $GITHUB_OUTPUT
fi
- name: Check for pending updates
id: pending
env:
CONF_NAME: ${{ steps.vars.outputs.conference }}
CONF_URL: ${{ steps.vars.outputs.url }}
UPDATE_BRANCH: auto/conference-updates
GH_TOKEN: ${{ github.token }}
run: |
# Bug #1 Fix (Revised): Check if any open PR modifies the same conference FILE
# This is more precise than fuzzy title/branch matching
# Logic: Find PRs that modify _data/conferences.yml, then check if their diff includes this conference
echo "::group::Checking for open PRs modifying ${CONF_NAME}"
MATCHING_PR_NUM=""
TARGET_FILE="_data/conferences.yml"
# Get all open PRs
OPEN_PRS=$(gh pr list --state open --json number,title --jq '.[].number' --limit 50 2>/dev/null || echo "")
if [ -n "$OPEN_PRS" ]; then
echo "Found $(echo "$OPEN_PRS" | wc -w) open PRs, checking which ones modify $TARGET_FILE..."
for PR_NUM in $OPEN_PRS; do
# Get files changed by this PR
CHANGED_FILES=$(gh pr view "$PR_NUM" --json files --jq '.files[].path' 2>/dev/null || echo "")
# Check if this PR modifies the conferences file
if echo "$CHANGED_FILES" | grep -q "^${TARGET_FILE}$"; then
echo "PR #$PR_NUM modifies $TARGET_FILE, checking if it affects ${CONF_NAME}..."
# Get the PR diff and search for this conference
# Use Python for safe string matching
PR_DIFF=$(gh pr diff "$PR_NUM" -- "$TARGET_FILE" 2>/dev/null || echo "")
if [ -n "$PR_DIFF" ]; then
export PR_DIFF
FOUND=$(python3 << 'DIFFSEARCH'
import os
import re
conf_name = os.environ.get('CONF_NAME', '').lower()
diff_content = os.environ.get('PR_DIFF', '').lower()
# Normalize conference name for matching
# "PyCon US" -> also match "pycon us", "pycon-us", etc.
conf_slug = re.sub(r'[^a-z0-9]+', '-', conf_name).strip('-')
conf_compact = re.sub(r'[^a-z0-9]+', '', conf_name)
# Check if this conference appears in the diff
if (conf_name in diff_content or
conf_slug in diff_content or
conf_compact in diff_content):
print("FOUND")
DIFFSEARCH
)
if [ "$FOUND" = "FOUND" ]; then
MATCHING_PR_NUM="$PR_NUM"
echo "PR #$PR_NUM modifies ${CONF_NAME}"
break
fi
fi
fi
done
if [ -z "$MATCHING_PR_NUM" ]; then
echo "Checked all PRs - none modify ${CONF_NAME} in $TARGET_FILE"
fi
else
echo "No open PRs found"
fi
if [ -n "$MATCHING_PR_NUM" ]; then
MATCHING_PR=$(gh pr view "$MATCHING_PR_NUM" --json title,url --jq '"\(.title) (\(.url))"' 2>/dev/null || echo "PR #$MATCHING_PR_NUM")
echo "has_pending=true" >> $GITHUB_OUTPUT
echo "pending_pr=$MATCHING_PR_NUM" >> $GITHUB_OUTPUT
echo "::notice title=Open PR Found::${CONF_NAME} already has an open PR: ${MATCHING_PR}"
echo "::endgroup::"
exit 0
fi
echo "::endgroup::"
# Also check the accumulator branch for pending commits
echo "::group::Checking accumulator branch for pending commits"
if git ls-remote --heads origin "$UPDATE_BRANCH" | grep -q "$UPDATE_BRANCH"; then
git fetch origin "$UPDATE_BRANCH" --depth=50
# Look for commits mentioning this conference that aren't in main yet
if git log "origin/main..origin/$UPDATE_BRANCH" --oneline --grep="conf: ${CONF_NAME}" 2>/dev/null | grep -q .; then
PENDING_COMMIT=$(git log "origin/main..origin/$UPDATE_BRANCH" --oneline --grep="conf: ${CONF_NAME}" -1)
# Check if there's an open PR from the update branch
OPEN_PR=$(gh pr list --head "$UPDATE_BRANCH" --state open --json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "$OPEN_PR" ]; then
echo "has_pending=true" >> $GITHUB_OUTPUT
echo "pending_pr=$OPEN_PR" >> $GITHUB_OUTPUT
echo "::notice title=Pending Update::${CONF_NAME} already has pending commit: ${PENDING_COMMIT} (PR #${OPEN_PR})"
else
echo "has_pending=false" >> $GITHUB_OUTPUT
echo "::notice title=Stale Branch::${CONF_NAME} has commit ${PENDING_COMMIT} but no open PR - proceeding with update"
fi
else
echo "has_pending=false" >> $GITHUB_OUTPUT
fi
else
echo "has_pending=false" >> $GITHUB_OUTPUT
fi
echo "::endgroup::"
- name: Detect monitoring zone
id: detect
env:
CONF_NAME: ${{ steps.vars.outputs.conference }}
FORCE_ZONE: ${{ inputs.force_zone }}
run: |
# Allow manual override
if [ -n "$FORCE_ZONE" ]; then
echo "zone=$FORCE_ZONE" >> $GITHUB_OUTPUT
if [ "$FORCE_ZONE" = "dead" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
echo "triage_mode=$FORCE_ZONE" >> $GITHUB_OUTPUT
echo "::notice title=Zone Override::Manually set to $FORCE_ZONE"
exit 0
fi
# Run zone detection (safe - uses env vars, not interpolation)
python3 << 'ZONECHECK' > /tmp/zone_check.txt 2>&1
import yaml
import os
import sys
from datetime import datetime, timedelta
conf_name = os.environ.get('CONF_NAME', '')
today = datetime.now().date()
# Load both conferences.yml and archive.yml
all_confs = []
try:
with open("_data/conferences.yml") as f:
confs = yaml.safe_load(f) or []
all_confs.extend(confs)
except FileNotFoundError:
print("INFO: conferences.yml not found", file=sys.stderr)
except yaml.YAMLError as e:
print(f"WARNING: Failed to parse conferences.yml: {e}", file=sys.stderr)
try:
with open("_data/archive.yml") as f:
archived = yaml.safe_load(f) or []
all_confs.extend(archived)
except FileNotFoundError:
print("INFO: archive.yml not found", file=sys.stderr)
except yaml.YAMLError as e:
print(f"WARNING: Failed to parse archive.yml: {e}", file=sys.stderr)
if not all_confs:
print("No conference data found - defaulting to NORMAL zone")
print("ZONE=normal")
print("SKIP=false")
print("TRIAGE_MODE=normal")
sys.exit(0)
# Find the most recent entry for this conference (highest year)
matching = [c for c in all_confs if conf_name.lower() in c.get("conference", "").lower()]
conf = max(matching, key=lambda x: x.get("year", 0)) if matching else None
if not conf:
print(f"Conference '{conf_name}' not found - NORMAL zone (discovery mode)")
print("ZONE=normal")
print("SKIP=false")
print("TRIAGE_MODE=normal")
sys.exit(0)
# Get CFP date (use extension if available)
cfp_str = conf.get("cfp_ext") or conf.get("cfp", "TBA")
has_extension = conf.get("cfp_ext") is not None
# Get conference end date
end_date = conf.get("end")
if isinstance(end_date, str):
end_date = datetime.strptime(end_date, "%Y-%m-%d").date()
# No CFP yet = NORMAL zone (actively hunting for CFP)
if cfp_str in ("TBA", "Cancelled", "None"):
print(f"No CFP announced yet for {conf_name} - NORMAL zone (hunting mode)")
print("ZONE=normal")
print("SKIP=false")
print("TRIAGE_MODE=normal")
sys.exit(0)
# Parse CFP date
try:
cfp_date = datetime.strptime(cfp_str[:10], "%Y-%m-%d").date()
except ValueError as e:
print(f"WARNING: Could not parse CFP date '{cfp_str}': {e}", file=sys.stderr)
print("ZONE=normal")
print("SKIP=false")
print("TRIAGE_MODE=normal")
sys.exit(0)
dead_zone_end = end_date + timedelta(days=60)
# CFP is still open = SLOW zone
if today <= cfp_date:
print(f"CFP open until {cfp_date} - SLOW zone (watching for extensions)")
print("ZONE=slow")
print("SKIP=false")
print("TRIAGE_MODE=slow")
sys.exit(0)
# CFP just closed (within 3 days) and no extension yet
extension_window = cfp_date + timedelta(days=3)
if today <= extension_window and not has_extension:
print(f"CFP just closed ({cfp_date}) - extension_watch zone")
print("ZONE=extension_watch")
print("SKIP=false")
print("TRIAGE_MODE=slow")
sys.exit(0)
# Between CFP close and conference end + 60 days = DEAD zone
if cfp_date < today <= dead_zone_end:
print(f"DEAD zone: CFP closed {cfp_date}, no updates until {dead_zone_end}")
print("ZONE=dead")
print("SKIP=true")
print("TRIAGE_MODE=dead")
sys.exit(0)
# Past dead zone = NORMAL (hunting for next year's CFP)
print(f"Past dead zone ({dead_zone_end}) - NORMAL zone (hunting next year)")
print("ZONE=normal")
print("SKIP=false")
print("TRIAGE_MODE=normal")
ZONECHECK
# Parse results
cat /tmp/zone_check.txt
ZONE=$(grep "^ZONE=" /tmp/zone_check.txt | cut -d= -f2 || echo "normal")
SKIP=$(grep "^SKIP=" /tmp/zone_check.txt | cut -d= -f2 || echo "false")
TRIAGE_MODE=$(grep "^TRIAGE_MODE=" /tmp/zone_check.txt | cut -d= -f2 || echo "normal")
echo "zone=${ZONE:-normal}" >> $GITHUB_OUTPUT
echo "skip=${SKIP:-false}" >> $GITHUB_OUTPUT
echo "triage_mode=${TRIAGE_MODE:-normal}" >> $GITHUB_OUTPUT
echo "::group::Zone Detection Results"
echo "Conference: $CONF_NAME"
echo "Zone: $ZONE"
echo "Skip: $SKIP"
echo "Triage mode: $TRIAGE_MODE"
echo "::endgroup::"
- name: Log zone detection
env:
CONFERENCE: ${{ steps.vars.outputs.conference }}
ZONE: ${{ steps.detect.outputs.zone }}
SKIP: ${{ steps.detect.outputs.skip }}
TRIAGE_MODE: ${{ steps.detect.outputs.triage_mode }}
run: |
echo "## Zone Detection" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Conference:** $CONFERENCE" >> $GITHUB_STEP_SUMMARY
echo "**Zone:** $ZONE" >> $GITHUB_STEP_SUMMARY
echo "**Skip:** $SKIP" >> $GITHUB_STEP_SUMMARY
echo "**Triage Mode:** $TRIAGE_MODE" >> $GITHUB_STEP_SUMMARY
# Stage 1: Quick triage using DIRECT API call (no agent overhead)
# This is ~10x cheaper than using claude-code-action for simple classification
triage:
needs: zone-check
if: needs.zone-check.outputs.skip != 'true' && needs.zone-check.outputs.has_pending != 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
should_analyze: ${{ steps.parse.outputs.should_analyze }}
triage_reason: ${{ steps.parse.outputs.reason }}
suggested_filters: ${{ steps.parse.outputs.suggested_filters }}
is_new_year: ${{ steps.parse.outputs.is_new_year }}
detected_year: ${{ steps.parse.outputs.detected_year }}
steps:
- name: Check if triage should be skipped
id: skip_check
env:
SKIP_TRIAGE: ${{ inputs.skip_triage }}
HAS_DIFF: ${{ needs.zone-check.outputs.has_diff }}
run: |
if [ "$SKIP_TRIAGE" = "true" ] || [ "$HAS_DIFF" = "false" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "::notice title=Triage Skipped::Manual trigger or no diff available"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
# Direct API call - much cheaper than agent mode for simple classification
- name: Quick triage via API
id: triage_api
if: steps.skip_check.outputs.skip != 'true'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CONF_URL: ${{ needs.zone-check.outputs.url }}
CONF_NAME: ${{ needs.zone-check.outputs.conference }}
DIFF_B64: ${{ needs.zone-check.outputs.diff_b64 }}
TRIAGE_MODE: ${{ needs.zone-check.outputs.triage_mode }}
# Trigger context from scheduled extension checker
TRIGGER_REASON: ${{ needs.zone-check.outputs.trigger_reason }}
ORIGINAL_CFP_DEADLINE: ${{ needs.zone-check.outputs.original_cfp_deadline }}
run: |
# Bug #2 Fix: Decode base64 diff content
# The diff is base64 encoded to safely pass multi-line content with special characters
if [ -n "$DIFF_B64" ]; then
DIFF_CONTENT=$(echo "$DIFF_B64" | base64 -d 2>/dev/null || echo "Failed to decode diff")
else
DIFF_CONTENT="No diff available"
fi
export DIFF_CONTENT
# Build the appropriate prompt based on trigger context and triage mode
# Priority: extension_check trigger > slow zone > general website change
# Export so Python subprocess can access it
if [ "$TRIGGER_REASON" = "extension_check" ]; then
# Triggered by scheduled extension checker - CFP deadline has passed
export PROMPT="You are checking for a CFP EXTENSION. The CFP deadline (${ORIGINAL_CFP_DEADLINE}) has passed. Focus ONLY on: 1) Whether the CFP was extended (new deadline announced), 2) Any 'extended', 'deadline moved', 'last chance' announcements. If you find a new CFP deadline date, mark RELEVANT and include the new date in REASON. If NO extension is mentioned, mark NOISE with REASON 'No CFP extension found'. Do NOT mark relevant for general website changes, schedules, speakers, etc."
echo "::notice title=Trigger Context::Scheduled CFP extension check (deadline was ${ORIGINAL_CFP_DEADLINE})"
elif [ "$TRIAGE_MODE" = "slow" ]; then
export PROMPT="You are a STRICT triage filter. This conference already has a CFP deadline - we ONLY care about EXTENSIONS. RELEVANT (extension indicators): extended, extension, new deadline, deadline moved, pushed back, more time, last chance. NOISE (everything else): speaker announcements, schedule, sponsors, registration, venue info. Be STRICT - if not clearly about deadline extension, mark NOISE."
else
export PROMPT="You are a triage filter for a Python conference tracker. NEW YEAR indicators: future year (2025/2026/2027) WITH SPECIFIC DATES (exact days like 'September 15-17', not just month or season). RELEVANT: CFP announcements with specific deadlines (exact date), specific conference dates announced (exact days). NOISE: copyright year only, timestamps, counters, typos, cookies, nav changes, sponsors, speakers, schedule. ALSO NOISE: Vague date announcements ('sometime in September', 'early 2026', 'Fall 2026', 'dates TBD', 'coming soon') - these are not actionable until specific dates are published."
fi
# Create the API request payload
cat > /tmp/request.json << 'PAYLOAD_END'
{
"model": "claude-haiku-4-5-20251001",
"max_tokens": 256,
"messages": [
{
"role": "user",
"content": "PROMPT_PLACEHOLDER\n\nConference: CONF_PLACEHOLDER\nURL: URL_PLACEHOLDER\n\nDIFF:\n```\nDIFF_PLACEHOLDER\n```\n\nRespond with EXACTLY this format:\nDECISION: RELEVANT or NOISE\nNEW_YEAR: YES or NO\nDETECTED_YEAR: [year or none]\nREASON: [one sentence]\nCONFIDENCE: HIGH, MEDIUM, or LOW\nSUGGESTED_FILTERS: [CSS selectors like '.footer, #nav, .countdown' and/or text patterns like 'text:Copyright, text:Last updated' - or 'none' if no filters needed. Do NOT use natural language descriptions.]"
}
]
}
PAYLOAD_END
# Use Python to safely substitute values (prevents injection)
python3 << 'PYSUBST'
import json
import os
with open('/tmp/request.json') as f:
data = json.load(f)
prompt = os.environ.get('PROMPT', '')
conf_name = os.environ.get('CONF_NAME', '')
conf_url = os.environ.get('CONF_URL', '')
diff = os.environ.get('DIFF_CONTENT', '')[:8000] # Limit diff size
content = data['messages'][0]['content']
content = content.replace('PROMPT_PLACEHOLDER', prompt)
content = content.replace('CONF_PLACEHOLDER', conf_name)
content = content.replace('URL_PLACEHOLDER', conf_url)
content = content.replace('DIFF_PLACEHOLDER', diff)
data['messages'][0]['content'] = content
with open('/tmp/request.json', 'w') as f:
json.dump(data, f)
PYSUBST
# Make the API call with retry for transient errors
echo "Calling Claude API for triage..."
for attempt in 1 2 3; do
HTTP_CODE=$(curl -s -w "%{http_code}" -o /tmp/response.json \
-X POST "https://api.anthropic.com/v1/messages" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d @/tmp/request.json)
# Success
if [ "$HTTP_CODE" = "200" ]; then
break
fi
# Retry on rate limit (429) or overloaded (529)
if [ "$HTTP_CODE" = "429" ] || [ "$HTTP_CODE" = "529" ]; then
echo "::warning title=API Rate Limited::HTTP $HTTP_CODE, attempt $attempt/3"
sleep $((attempt * 5))
continue
fi
# Other errors - don't retry
break
done
if [ "$HTTP_CODE" != "200" ]; then
echo "::warning title=API Error::HTTP $HTTP_CODE - falling back to full analysis"
echo "success=false" >> $GITHUB_OUTPUT
cat /tmp/response.json >&2
exit 0
fi
# Extract the response text
RESPONSE=$(jq -r '.content[0].text // ""' /tmp/response.json)
if [ -z "$RESPONSE" ]; then
echo "::warning title=Empty Response::No content in API response"
echo "success=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "success=true" >> $GITHUB_OUTPUT
# Save response for parsing
DELIM="RESP_EOF_$(date +%s%N)_$$"
echo "response<<$DELIM" >> $GITHUB_OUTPUT
echo "$RESPONSE" >> $GITHUB_OUTPUT
echo "$DELIM" >> $GITHUB_OUTPUT
# Log token usage (use awk instead of bc for portability)
INPUT_TOKENS=$(jq -r '.usage.input_tokens // 0' /tmp/response.json)
OUTPUT_TOKENS=$(jq -r '.usage.output_tokens // 0' /tmp/response.json)
echo "::group::API Usage"
echo "Input tokens: $INPUT_TOKENS"
echo "Output tokens: $OUTPUT_TOKENS"
COST=$(awk "BEGIN {printf \"%.6f\", $INPUT_TOKENS * 0.00000025 + $OUTPUT_TOKENS * 0.00000125}")
echo "Estimated cost: \$$COST"
echo "::endgroup::"
# Cleanup temp files (security best practice)
rm -f /tmp/request.json
- name: Parse triage result
id: parse
env:
SKIP_CHECK: ${{ steps.skip_check.outputs.skip }}
API_SUCCESS: ${{ steps.triage_api.outputs.success }}
TRIAGE_RESPONSE: ${{ steps.triage_api.outputs.response }}
TRIGGER_REASON: ${{ needs.zone-check.outputs.trigger_reason }}
ORIGINAL_CFP_DEADLINE: ${{ needs.zone-check.outputs.original_cfp_deadline }}
CONF_NAME: ${{ needs.zone-check.outputs.conference }}
run: |
# Default to analyzing if triage was skipped
if [ "$SKIP_CHECK" = "true" ]; then
echo "should_analyze=true" >> $GITHUB_OUTPUT
echo "reason=Triage skipped - manual trigger or no diff" >> $GITHUB_OUTPUT
echo "suggested_filters=" >> $GITHUB_OUTPUT
echo "is_new_year=false" >> $GITHUB_OUTPUT
echo "detected_year=" >> $GITHUB_OUTPUT
exit 0
fi
# Default to analyzing if API call failed
if [ "$API_SUCCESS" != "true" ]; then
echo "should_analyze=true" >> $GITHUB_OUTPUT
echo "reason=Triage API failed - analyzing as fallback" >> $GITHUB_OUTPUT
echo "suggested_filters=" >> $GITHUB_OUTPUT
echo "is_new_year=false" >> $GITHUB_OUTPUT
echo "detected_year=" >> $GITHUB_OUTPUT
exit 0
fi
echo "::group::Triage Response"
echo "$TRIAGE_RESPONSE"
echo "::endgroup::"
# Parse DECISION (case-insensitive, whitespace-tolerant)
if echo "$TRIAGE_RESPONSE" | grep -iqE "DECISION:\s*RELEVANT"; then
echo "should_analyze=true" >> $GITHUB_OUTPUT
elif echo "$TRIAGE_RESPONSE" | grep -iqE "DECISION:\s*NOISE"; then
# Check confidence - if LOW, analyze anyway
if echo "$TRIAGE_RESPONSE" | grep -iqE "CONFIDENCE:\s*LOW"; then
echo "should_analyze=true" >> $GITHUB_OUTPUT
echo "::notice title=Low Confidence::Analyzing despite NOISE decision"
else
echo "should_analyze=false" >> $GITHUB_OUTPUT
# Log "no extension found" specifically for extension checks
if [ "$TRIGGER_REASON" = "extension_check" ]; then
echo "::notice title=No Extension Found::${CONF_NAME} - CFP deadline ${ORIGINAL_CFP_DEADLINE} was NOT extended"
echo "## ✓ No Extension Found" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Conference:** ${CONF_NAME}" >> $GITHUB_STEP_SUMMARY
echo "**Original CFP deadline:** ${ORIGINAL_CFP_DEADLINE}" >> $GITHUB_STEP_SUMMARY
echo "**Result:** No CFP extension announced - no PR needed" >> $GITHUB_STEP_SUMMARY
fi
fi
else
echo "should_analyze=true" >> $GITHUB_OUTPUT
echo "::warning title=Parse Error::Could not parse DECISION, defaulting to analyze"
fi
# Parse NEW_YEAR
if echo "$TRIAGE_RESPONSE" | grep -iqE "NEW_YEAR:\s*YES"; then
echo "is_new_year=true" >> $GITHUB_OUTPUT
else
echo "is_new_year=false" >> $GITHUB_OUTPUT
fi
# Parse DETECTED_YEAR
DETECTED_YEAR=$(echo "$TRIAGE_RESPONSE" | grep -iE "DETECTED_YEAR:" | sed 's/.*DETECTED_YEAR:\s*//i' | head -1 | tr -d ' ')
if [ "$DETECTED_YEAR" = "none" ] || [ -z "$DETECTED_YEAR" ]; then
echo "detected_year=" >> $GITHUB_OUTPUT
else
echo "detected_year=$DETECTED_YEAR" >> $GITHUB_OUTPUT
fi
# Extract reason
REASON=$(echo "$TRIAGE_RESPONSE" | grep -iE "REASON:" | sed 's/.*REASON:\s*//i' | head -1)
echo "reason=${REASON:-Triage completed}" >> $GITHUB_OUTPUT
# Extract suggested filters
FILTERS=$(echo "$TRIAGE_RESPONSE" | grep -iE "SUGGESTED_FILTERS:" | sed 's/.*SUGGESTED_FILTERS:\s*//i' | head -1)
echo "suggested_filters=${FILTERS:-}" >> $GITHUB_OUTPUT
# Apply filters directly to changedetection.io
apply-filters:
needs: [zone-check, triage]
if: |
always() &&
needs.triage.outputs.should_analyze == 'false' &&
needs.triage.outputs.suggested_filters != 'none' &&
needs.triage.outputs.suggested_filters != ''
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
issues: write
steps:
- name: Apply filters via API
id: apply_api
if: vars.CHANGEDETECTION_URL != '' && needs.zone-check.outputs.watch_uuid != ''
env:
WATCH_UUID: ${{ needs.zone-check.outputs.watch_uuid }}
FILTERS: ${{ needs.triage.outputs.suggested_filters }}
CHANGEDETECTION_URL: ${{ vars.CHANGEDETECTION_URL }}
CHANGEDETECTION_KEY: ${{ secrets.CHANGEDETECTION_KEY }}
CF_ACCESS_CLIENT_ID: ${{ secrets.CF_ACCESS_CLIENT_ID }}
CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF_ACCESS_CLIENT_SECRET }}
run: |
if [ -z "$FILTERS" ] || [ "$FILTERS" = "none" ]; then
echo "No filters to apply"
echo "success=skipped" >> $GITHUB_OUTPUT
exit 0
fi
# Validate filter format: must contain CSS selectors (starting with . # or tag names)
# or text: patterns. Reject natural language descriptions.
# Valid examples: ".footer, #nav, text:Copyright"
# Invalid: "Filter out pages discussing..."
if ! echo "$FILTERS" | grep -qE '^\s*([.#]|text:|[a-z]+\[|[a-z]+\s*,|[a-z]+\s*$)' ; then
# Check if it looks like natural language (contains common words)
if echo "$FILTERS" | grep -qiE '\b(filter|out|pages|discussing|announcements|containing|patterns|deadline)\b'; then
echo "::warning::Filters appear to be natural language, not CSS selectors. Skipping."
echo "success=skipped" >> $GITHUB_OUTPUT
exit 0
fi
fi
# Parse CSS selectors and text patterns
CSS_FILTERS=$(echo "$FILTERS" | tr ',' '\n' | grep -v 'text:' | sed 's/^ *//' | tr '\n' ',' | sed 's/,$//')
TEXT_PATTERNS=$(echo "$FILTERS" | tr ',' '\n' | grep 'text:' | sed 's/text://' | sed 's/^ *//' | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "::group::Filter Details"
echo "CSS filters: $CSS_FILTERS"
echo "Text patterns: $TEXT_PATTERNS"
echo "::endgroup::"
# Fetch current config
CURRENT=$(curl -sf \
-H "x-api-key: $CHANGEDETECTION_KEY" \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
"$CHANGEDETECTION_URL/api/v1/watch/$WATCH_UUID") || {
echo "::warning::Failed to fetch watch config"
echo "success=false" >> $GITHUB_OUTPUT
exit 0
}
# Merge filters
EXISTING_CSS=$(echo "$CURRENT" | jq -r '.subtractive_selectors // ""')
if [ -n "$EXISTING_CSS" ] && [ "$EXISTING_CSS" != "null" ] && [ -n "$CSS_FILTERS" ]; then
MERGED_CSS="$EXISTING_CSS, $CSS_FILTERS"
elif [ -n "$CSS_FILTERS" ]; then
MERGED_CSS="$CSS_FILTERS"
else
MERGED_CSS="$EXISTING_CSS"
fi
EXISTING_TEXT=$(echo "$CURRENT" | jq -c '.ignore_text // []' 2>/dev/null) || EXISTING_TEXT="[]"
# Ensure EXISTING_TEXT is valid JSON array
if ! echo "$EXISTING_TEXT" | jq -e 'type == "array"' > /dev/null 2>&1; then
EXISTING_TEXT="[]"
fi
# Ensure TEXT_PATTERNS is valid JSON array
if [ -z "$TEXT_PATTERNS" ] || ! echo "$TEXT_PATTERNS" | jq -e 'type == "array"' > /dev/null 2>&1; then
TEXT_PATTERNS="[]"
fi
if [ "$TEXT_PATTERNS" != "[]" ]; then
MERGED_TEXT=$(echo "$EXISTING_TEXT $TEXT_PATTERNS" | jq -s 'add | unique' 2>/dev/null) || MERGED_TEXT="$EXISTING_TEXT"
else
MERGED_TEXT="$EXISTING_TEXT"
fi
# Final validation: ensure MERGED_TEXT is valid JSON array
if ! echo "$MERGED_TEXT" | jq -e 'type == "array"' > /dev/null 2>&1; then
MERGED_TEXT="[]"
fi
# Apply update
UPDATE_PAYLOAD=$(jq -n --arg css "$MERGED_CSS" --argjson text "$MERGED_TEXT" '{subtractive_selectors: $css, ignore_text: $text}')
curl -sf -X PUT \
-H "x-api-key: $CHANGEDETECTION_KEY" \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d "$UPDATE_PAYLOAD" \
"$CHANGEDETECTION_URL/api/v1/watch/$WATCH_UUID" && {
echo "success=true" >> $GITHUB_OUTPUT
echo "::notice::Filters applied successfully"
} || {
echo "success=false" >> $GITHUB_OUTPUT
echo "::warning::Failed to apply filters"
}
- name: Log unapplied filter suggestion
if: steps.apply_api.outputs.success == 'false' || vars.CHANGEDETECTION_URL == ''
env:
CONF_NAME: ${{ needs.zone-check.outputs.conference }}
TRIAGE_FILTERS: ${{ needs.triage.outputs.suggested_filters }}
run: |
echo "::warning title=Filter Not Applied::Could not apply filters for ${CONF_NAME}: ${TRIAGE_FILTERS}"
# Stage 2: Full analysis with Claude Code Action (needs file editing tools)
analyze-update:
needs: [zone-check, triage]
if: |
needs.zone-check.outputs.skip != 'true' &&
needs.zone-check.outputs.has_pending != 'true' &&
needs.triage.outputs.should_analyze == 'true' &&
needs.triage.outputs.is_new_year != 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
id-token: write
env:
UPDATE_BRANCH: auto/conference-updates
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ github.token }}
persist-credentials: true
- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Setup accumulator branch
run: |
git remote set-url origin "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git"
if git ls-remote --heads origin $UPDATE_BRANCH | grep -q $UPDATE_BRANCH; then
git fetch origin $UPDATE_BRANCH
git checkout $UPDATE_BRANCH
# Try rebase first, then merge, fail if neither works
if ! git rebase origin/main; then
echo "::warning title=Rebase Failed::Attempting merge instead"
git rebase --abort 2>/dev/null || true
if ! git merge origin/main --no-edit; then
git merge --abort 2>/dev/null || true
echo "::warning title=Merge Conflict::Auto-resolving conflicts in favor of main"
if ! git merge origin/main -X theirs --no-edit; then
git merge --abort 2>/dev/null || true
echo "::error title=Branch Sync Failed::Could not sync with main"
exit 1
fi
fi
fi
else
git checkout -b $UPDATE_BRANCH
fi
- name: Snapshot conferences.yml
run: cp _data/conferences.yml /tmp/conferences_before.yml
- name: Prepare content
id: content
env:
DIFF_B64: ${{ needs.zone-check.outputs.diff_b64 }}
CONF_URL: ${{ needs.zone-check.outputs.url }}
HAS_DIFF: ${{ needs.zone-check.outputs.has_diff }}
run: |
# Bug #2 Fix: Write content to a temp file instead of using heredoc
# This prevents delimiter collision when scraped content contains special characters
# Claude Code Action can read the file using its Read tool
CONTENT_FILE="${RUNNER_TEMP}/website_content.txt"
# Decode base64 diff if available
if [ "$HAS_DIFF" = "true" ] && [ -n "$DIFF_B64" ]; then
DIFF_CONTENT=$(echo "$DIFF_B64" | base64 -d 2>/dev/null || echo "")
DIFF_LENGTH=${#DIFF_CONTENT}
else
DIFF_CONTENT=""
DIFF_LENGTH=0
fi
if [ "$DIFF_LENGTH" -gt 100 ]; then
echo "source=diff" >> $GITHUB_OUTPUT
printf '%s' "$DIFF_CONTENT" > "$CONTENT_FILE"
else
echo "source=fetched" >> $GITHUB_OUTPUT
# Use lynx for clean text extraction (much fewer tokens than raw HTML)
sudo apt-get install -qq -y lynx > /dev/null 2>&1 || true
lynx -dump -nolist -nonumbers -width=200 "$CONF_URL" 2>/dev/null | head -c 6000 > "$CONTENT_FILE" || echo "Failed to fetch content" > "$CONTENT_FILE"
fi
# Store the file path as output (safe - no special characters)
echo "content_file=$CONTENT_FILE" >> $GITHUB_OUTPUT
echo "::notice title=Content::Stored $(wc -c < "$CONTENT_FILE") bytes in $CONTENT_FILE"
# Workaround for anthropics/claude-code-action#900: the action's
# checkHumanActor calls the GitHub API before checking allowed_bots,
# which 404s for bot actors like github-actions[bot]. Override
# GITHUB_ACTOR so the action sees a real user.
- name: Fix actor for bot-triggered runs
if: contains(github.actor, '[bot]')
run: echo "GITHUB_ACTOR=${{ github.repository_owner }}" >> "$GITHUB_ENV"
- name: Run Claude Code Analysis
uses: anthropics/claude-code-action@v1.0.189
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_bots: 'github-actions[bot]'
prompt: |
Update pythondeadlin.es conference data. DO NOT use Task tool or fetch URLs.
Conference: ${{ needs.zone-check.outputs.conference }}
URL: ${{ needs.zone-check.outputs.url }}
Zone: ${{ needs.zone-check.outputs.zone }}
Reason: ${{ needs.triage.outputs.triage_reason }}
FIRST: Use the Read tool to read the website content from: ${{ steps.content.outputs.content_file }}
This file contains either the diff of recent changes or the fetched website content.
Task: Read _data/conferences.yml, find this conference, update if needed.
If zone is slow/extension_watch: ONLY update cfp_ext for extensions.
If zone is normal: update CFP dates, conference dates, URLs.
Schema: conference (no year), year (int), link, cfp ('YYYY-MM-DD HH:mm:ss' or 'TBA'), cfp_ext (extended deadline), place ("City, Country"), start/end (YYYY-MM-DD), sub (PY|SCIPY|DATA|WEB|BIZ|GEO)
CFP Extension: Keep original cfp, set cfp_ext to new deadline.
CRITICAL: Only update dates if you find SPECIFIC dates (e.g., "September 15-17, 2026"). If dates are vague ("sometime in September", "Fall 2026", "TBD"), do NOT update the entry. Report "No changes - specific dates not found" and make no edits. Never fabricate or approximate dates.
claude_args: |
--model claude-haiku-4-5-20251001
--max-turns 5
--allowedTools Read,Edit,Grep,Glob
- name: Check for changes
id: check_changes
run: |
if ! diff -q _data/conferences.yml /tmp/conferences_before.yml > /dev/null 2>&1; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
- name: Setup Pixi
if: steps.check_changes.outputs.changed == 'true'
uses: prefix-dev/setup-pixi@v0.10.0
- name: Sort conferences
if: steps.check_changes.outputs.changed == 'true'
run: pixi run sort
- name: Commit and push
if: steps.check_changes.outputs.changed == 'true'
id: commit
env:
CONF_NAME: ${{ needs.zone-check.outputs.conference }}
CONF_URL: ${{ needs.zone-check.outputs.url }}
CONF_ZONE: ${{ needs.zone-check.outputs.zone }}
run: |
git remote set-url origin "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git"
git add _data/conferences.yml
git commit -m "conf: ${CONF_NAME}" -m "Source: ${CONF_URL}" -m "Zone: ${CONF_ZONE}"
git push origin $UPDATE_BRANCH --force-with-lease
echo "commit_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Create or update PR
if: steps.check_changes.outputs.changed == 'true'
uses: actions/github-script@v9
env:
UPDATE_BRANCH: ${{ env.UPDATE_BRANCH }}
CONF_NAME: ${{ needs.zone-check.outputs.conference }}
COMMIT_SHA: ${{ steps.commit.outputs.commit_sha }}
with:
script: |
const branch = process.env.UPDATE_BRANCH;
const conference = process.env.CONF_NAME;
const sha = process.env.COMMIT_SHA.substring(0, 7);
const date = new Date().toISOString().split('T')[0];
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.repo.owner}:${branch}`,
state: 'open'
});
const entry = `| ${conference} | [${sha}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${process.env.COMMIT_SHA}) | ${date} |`;
if (prs.length > 0) {
const pr = prs[0];