Files
nexu-io-open-design/.github/workflows/tutorials-youtube-sync.yml
xne998808-ai 88d1eab743 feat(landing): surface user-submitted tutorial issues and PRs in the daily digest (#4095)
* feat(landing): surface user-submitted tutorial issues and PRs in the digest

User submissions lived only on GitHub and never entered the daily review flow.
The tutorials-youtube-sync digest now also lists them so they're reviewed
alongside auto-discovered YouTube candidates:

- Submission issues: open issues from the 'Submit a tutorial' form (label
  tutorials). The card shows each with the video link extracted from the body;
  on approval a maintainer generates the entry and opens a PR (Closes #issue).
- Contribution PRs: open PRs touching app/content/tutorials/** are auto-labeled
  'tutorials' by a new labeler workflow, and listed in the card for review/merge
  on GitHub.

Adds github-submissions.ts (search-API fetch + video-URL extraction) with unit
tests, two new Feishu card sections, issues:read + pull-requests:read on the
workflow, and the labeler workflow/config. Submission lookup is non-fatal — a
failure skips those sections without affecting the YouTube candidates.

* fix(landing): fail on submission lookup error; broaden YouTube URL parsing

Per review:
- A failed submission search no longer silently posts a digest without the
  issue/PR sections. It now aborts before posting (exit non-zero, observable);
  submissions aren't windowed so the next run re-queries them, and YouTube
  candidates are re-covered via the unchanged watermark — nothing is lost.
- Extract a shared extractYouTubeId() (watch incl. m.youtube, youtu.be, embed,
  shorts, live) used by both the submission parser and generate-selected, so the
  digest no longer mislabels shorts/mobile links as 'no video link'. videoUrl is
  canonicalized to youtu.be/<id>.
- Tests: shorts + mobile-watch extraction, and that a failed search rejects
  (is not swallowed).

* fix(landing): host-anchor extractYouTubeId to real YouTube hosts

Per review: the broadened matcher accepted ?v=<id> on any host, so a non-YouTube
link like https://example.com/watch?v=... was canonicalized to a youtu.be URL
and hid invalid submissions. Require a YouTube host (youtube.com / m. / music. /
youtu.be) before accepting v=/shorts/embed/live. Add a regression test that a
non-YouTube ?v= URL leaves videoUrl undefined.

* fix(landing): validate YouTube host via URL hostname, not substring

Per review: the substring host check matched lookalikes (notyoutube.com,
evil-youtube.com). extractYouTubeId now parses candidate URLs with new URL() and
checks hostname against an allowlist (youtu.be, youtube.com, www/m/music
.youtube.com) before extracting the id. Regression fixtures for non-YouTube and
lookalike hosts.

* fix(ci): grant issues:write to labeler so it can create the tutorials label

Per review: actions/labeler needs issue-label write to create the tutorials
label when missing. The label is also pre-created in the repo so the
issue-submission form can apply it; this makes first-run deterministic.

* fix(landing): sync tutorials label to live diff; paginate submission search

Per review:
- labeler: sync-labels: true so the tutorials label is removed when a PR no
  longer touches the tutorials content dir, keeping the digest's
  is:pr label:tutorials query free of stale/non-tutorial PRs.
- searchIssues paginates (per_page=100, loops until a short page, 10-page cap)
  so a backlog over one page is not silently truncated. Test stubs two pages.

---------

Co-authored-by: Joey <276262049+xne998808-ai@users.noreply.github.com>
2026-06-10 14:18:29 +00:00

66 lines
2.3 KiB
YAML

name: tutorials-youtube-sync
on:
# Daily sweep for new community YouTube tutorials about Open Design. New
# candidates are posted to Feishu for a maintainer to review — this workflow
# does NOT generate entries or open PRs. A maintainer replies with which
# candidates to publish, and the selected videos are turned into entries via
# scripts/youtube-tutorials/generate-selected.ts (run by a maintainer, who
# opens the PR).
schedule:
- cron: '30 2 * * *'
# Manual trigger: force a sweep outside the daily schedule. Leave days empty to
# use the same gap-free watermark (since the last successful run) the schedule
# uses; set it to force a fixed N-day catch-up window.
workflow_dispatch:
inputs:
days:
description: 'Override lookback window in days (blank = since last successful run)'
required: false
default: ''
permissions:
contents: read
# Read this workflow's own run history to derive a gap-free window start.
actions: read
# Read open tutorial-submission issues and contribution PRs for the digest.
issues: read
pull-requests: read
concurrency:
group: tutorials-youtube-sync
cancel-in-progress: true
jobs:
notify:
name: Post tutorial candidates to Feishu
if: github.repository == 'nexu-io/open-design'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Discover candidates and post Feishu digest
working-directory: apps/landing-page
env:
GITHUB_TOKEN: ${{ github.token }}
YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_AUTH_TOKEN: ${{ secrets.ANTHROPIC_AUTH_TOKEN }}
ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }}
FEISHU_TUTORIALS_WEBHOOK: ${{ secrets.FEISHU_TUTORIALS_WEBHOOK }}
FEISHU_TUTORIALS_SECRET: ${{ secrets.FEISHU_TUTORIALS_SECRET }}
DAYS_OVERRIDE: ${{ github.event.inputs.days }}
run: |
if [ -n "$DAYS_OVERRIDE" ]; then
npx --yes tsx@4.22.3 scripts/youtube-tutorials/notify-candidates.ts --days "$DAYS_OVERRIDE"
else
npx --yes tsx@4.22.3 scripts/youtube-tutorials/notify-candidates.ts
fi