[{"data":1,"prerenderedAt":932},["ShallowReactive",2],{"post-2025-02-23-monitor-copr-build-state-with-cloudflare-workers-en":3,"surround-2025-02-23-monitor-copr-build-state-with-cloudflare-workers-en":923,"randomIndex-year-month-day-slug___en-{\"year\":\"2025\",\"month\":\"02\",\"day\":\"23\",\"slug\":\"monitor-copr-build-state-with-cloudflare-workers\"}":254,"language-switch-year-month-day-slug___en-{\"year\":\"2025\",\"month\":\"02\",\"day\":\"23\",\"slug\":\"monitor-copr-build-state-with-cloudflare-workers\"}-zh":931},{"title":4,"date":5,"path":6,"tags":7,"body":11,"description":74},"Monitoring Fedora Copr Build Status with Cloudflare Workers","2025-02-23 12:12:53","/2025/02/23/monitor-copr-build-state-with-cloudflare-workers",[8,9,10],"JavaScript","Cloudflare","Fedora",{"type":12,"value":13,"toc":921},"minimark",[14,24,36,55,69,76,79,82,340,343,540,543,898,901,906,917],[15,16,17],"blockquote",{},[18,19,20],"p",{},[21,22,23],"del",{},"Admittedly, I've gotten addicted to Cloudflare Workers",[18,25,26,27,35],{},"In my earlier post ",[28,29,31],"a",{"href":30},"/2024/04/29/update-a-rpm-spec-by-github-action/",[32,33,34],"em",{},"\"Updating RPM Spec Files for Packaging with GitHub Actions\"",", I used GitHub Actions to automate spec file version bumps. Combined with Fedora Copr's webhook support, this enables fully automated package builds. It seemed perfect — except there was no build status monitoring, meaning I had no way to get timely notifications when a build failed (whether due to a bug in the spec itself or a network issue during the build).",[18,37,38,44,45,50,51,54],{},[28,39,43],{"href":40,"rel":41},"https://yanqiyu.info",[42],"nofollow","Nishikino Carbonyl"," suggested ",[28,46,49],{"href":47,"rel":48},"https://notifications.fedoraproject.org/",[42],"notifications.fedoraproject.org",", which supports configuring notifications — there's even a Copr option under Filters > Applications. Unfortunately, it didn't work in practice. The notification settings there appear to only configure ",[32,52,53],{},"email filtering rules"," — if Copr never intended to send you an email on build failure in the first place, no filter rule will make one appear.",[18,56,57,58,63,64,68],{},"Fortunately, Fedora Copr has a well-documented ",[28,59,62],{"href":60,"rel":61},"https://copr.fedorainfracloud.org/api_3/docs",[42],"API",". The ",[65,66,67],"code",{},"/monitor"," endpoint can be used to fetch the latest build status for packages.",[18,70,71],{},[72,73],"img",{"alt":74,"src":75},"","https://static.031130.xyz/uploads/2025/02/23/637811d2d85f6.webp",[18,77,78],{},"So the plan is: use a Cloudflare Workers cron job to periodically call this endpoint and check for any failed builds.",[18,80,81],{},"Let's start with the fetch logic:",[83,84,88],"pre",{"className":85,"code":86,"language":87,"meta":74,"style":74},"language-javascript shiki shiki-themes one-light one-dark-pro","async function fetchCopr() {\n    const ownername = \"zhullyb\";\n    const projectname = \"v2rayA\";\n    const url = new URL(\"https://copr.fedorainfracloud.org/api_3/monitor\")\n    url.searchParams.set(\"ownername\", ownername)\n    url.searchParams.set(\"projectname\", projectname)\n    const response = await fetch(url)\n    const data = await response.json()\n    if (data.output !== \"ok\") {\n        throw new Error(\"Failed to fetch COPR data\")\n    }\n    return data\n}\n","javascript",[65,89,90,110,131,146,171,204,229,252,274,301,319,325,334],{"__ignoreMap":74},[91,92,95,99,102,106],"span",{"class":93,"line":94},"line",1,[91,96,98],{"class":97},"sLKXg","async",[91,100,101],{"class":97}," function",[91,103,105],{"class":104},"sAdtL"," fetchCopr",[91,107,109],{"class":108},"s5ixo","() {\n",[91,111,113,116,120,124,128],{"class":93,"line":112},2,[91,114,115],{"class":97},"    const",[91,117,119],{"class":118},"sNmU0"," ownername",[91,121,123],{"class":122},"s_Sar"," =",[91,125,127],{"class":126},"sDhpE"," \"zhullyb\"",[91,129,130],{"class":108},";\n",[91,132,134,136,139,141,144],{"class":93,"line":133},3,[91,135,115],{"class":97},[91,137,138],{"class":118}," projectname",[91,140,123],{"class":122},[91,142,143],{"class":126}," \"v2rayA\"",[91,145,130],{"class":108},[91,147,149,151,154,156,159,162,165,168],{"class":93,"line":148},4,[91,150,115],{"class":97},[91,152,153],{"class":118}," url",[91,155,123],{"class":122},[91,157,158],{"class":97}," new",[91,160,161],{"class":104}," URL",[91,163,164],{"class":108},"(",[91,166,167],{"class":126},"\"https://copr.fedorainfracloud.org/api_3/monitor\"",[91,169,170],{"class":108},")\n",[91,172,174,178,181,185,187,190,192,195,198,202],{"class":93,"line":173},5,[91,175,177],{"class":176},"s7GmK","    url",[91,179,180],{"class":108},".",[91,182,184],{"class":183},"s2QsP","searchParams",[91,186,180],{"class":108},[91,188,189],{"class":104},"set",[91,191,164],{"class":108},[91,193,194],{"class":126},"\"ownername\"",[91,196,197],{"class":108},", ",[91,199,201],{"class":200},"sz0mV","ownername",[91,203,170],{"class":108},[91,205,207,209,211,213,215,217,219,222,224,227],{"class":93,"line":206},6,[91,208,177],{"class":176},[91,210,180],{"class":108},[91,212,184],{"class":183},[91,214,180],{"class":108},[91,216,189],{"class":104},[91,218,164],{"class":108},[91,220,221],{"class":126},"\"projectname\"",[91,223,197],{"class":108},[91,225,226],{"class":200},"projectname",[91,228,170],{"class":108},[91,230,232,234,237,239,242,245,247,250],{"class":93,"line":231},7,[91,233,115],{"class":97},[91,235,236],{"class":118}," response",[91,238,123],{"class":122},[91,240,241],{"class":97}," await",[91,243,244],{"class":104}," fetch",[91,246,164],{"class":108},[91,248,249],{"class":200},"url",[91,251,170],{"class":108},[91,253,255,257,260,262,264,266,268,271],{"class":93,"line":254},8,[91,256,115],{"class":97},[91,258,259],{"class":118}," data",[91,261,123],{"class":122},[91,263,241],{"class":97},[91,265,236],{"class":176},[91,267,180],{"class":108},[91,269,270],{"class":104},"json",[91,272,273],{"class":108},"()\n",[91,275,277,280,283,286,288,292,295,298],{"class":93,"line":276},9,[91,278,279],{"class":97},"    if",[91,281,282],{"class":108}," (",[91,284,285],{"class":176},"data",[91,287,180],{"class":108},[91,289,291],{"class":290},"sJa8x","output",[91,293,294],{"class":122}," !==",[91,296,297],{"class":126}," \"ok\"",[91,299,300],{"class":108},") {\n",[91,302,304,307,309,312,314,317],{"class":93,"line":303},10,[91,305,306],{"class":97},"        throw",[91,308,158],{"class":97},[91,310,311],{"class":104}," Error",[91,313,164],{"class":108},[91,315,316],{"class":126},"\"Failed to fetch COPR data\"",[91,318,170],{"class":108},[91,320,322],{"class":93,"line":321},11,[91,323,324],{"class":108},"    }\n",[91,326,328,331],{"class":93,"line":327},12,[91,329,330],{"class":97},"    return",[91,332,333],{"class":200}," data\n",[91,335,337],{"class":93,"line":336},13,[91,338,339],{"class":108},"}\n",[18,341,342],{},"Next, the notification logic. I'm using a Feishu (Lark) webhook bot here:",[83,344,346],{"className":85,"code":345,"language":87,"meta":74,"style":74},"async function notify(text) {\n    const webhook = \"https://open.feishu.cn/open-apis/bot/v2/hook/ffffffff-ffff-ffff-ffff-ffffffffffff\"\n    const body = {\n        msg_type: \"text\",\n        content: {\n            text: text\n        }\n    }\n    const response = await fetch(webhook, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\"\n        },\n        body: JSON.stringify(body)\n    })\n    console.log(response)\n}\n",[65,347,348,365,377,389,404,413,423,428,432,452,464,473,483,488,511,517,535],{"__ignoreMap":74},[91,349,350,352,354,357,359,363],{"class":93,"line":94},[91,351,98],{"class":97},[91,353,101],{"class":97},[91,355,356],{"class":104}," notify",[91,358,164],{"class":108},[91,360,362],{"class":361},"s8iYz","text",[91,364,300],{"class":108},[91,366,367,369,372,374],{"class":93,"line":112},[91,368,115],{"class":97},[91,370,371],{"class":118}," webhook",[91,373,123],{"class":122},[91,375,376],{"class":126}," \"https://open.feishu.cn/open-apis/bot/v2/hook/ffffffff-ffff-ffff-ffff-ffffffffffff\"\n",[91,378,379,381,384,386],{"class":93,"line":133},[91,380,115],{"class":97},[91,382,383],{"class":118}," body",[91,385,123],{"class":122},[91,387,388],{"class":108}," {\n",[91,390,391,394,398,401],{"class":93,"line":148},[91,392,393],{"class":290},"        msg_type",[91,395,397],{"class":396},"st7oF",":",[91,399,400],{"class":126}," \"text\"",[91,402,403],{"class":108},",\n",[91,405,406,409,411],{"class":93,"line":173},[91,407,408],{"class":290},"        content",[91,410,397],{"class":396},[91,412,388],{"class":108},[91,414,415,418,420],{"class":93,"line":206},[91,416,417],{"class":290},"            text",[91,419,397],{"class":396},[91,421,422],{"class":200}," text\n",[91,424,425],{"class":93,"line":231},[91,426,427],{"class":108},"        }\n",[91,429,430],{"class":93,"line":254},[91,431,324],{"class":108},[91,433,434,436,438,440,442,444,446,449],{"class":93,"line":276},[91,435,115],{"class":97},[91,437,236],{"class":118},[91,439,123],{"class":122},[91,441,241],{"class":97},[91,443,244],{"class":104},[91,445,164],{"class":108},[91,447,448],{"class":200},"webhook",[91,450,451],{"class":108},", {\n",[91,453,454,457,459,462],{"class":93,"line":303},[91,455,456],{"class":290},"        method",[91,458,397],{"class":396},[91,460,461],{"class":126}," \"POST\"",[91,463,403],{"class":108},[91,465,466,469,471],{"class":93,"line":321},[91,467,468],{"class":290},"        headers",[91,470,397],{"class":396},[91,472,388],{"class":108},[91,474,475,478,480],{"class":93,"line":327},[91,476,477],{"class":126},"            \"Content-Type\"",[91,479,397],{"class":396},[91,481,482],{"class":126}," \"application/json\"\n",[91,484,485],{"class":93,"line":336},[91,486,487],{"class":108},"        },\n",[91,489,491,494,496,499,501,504,506,509],{"class":93,"line":490},14,[91,492,493],{"class":290},"        body",[91,495,397],{"class":396},[91,497,498],{"class":118}," JSON",[91,500,180],{"class":108},[91,502,503],{"class":104},"stringify",[91,505,164],{"class":108},[91,507,508],{"class":200},"body",[91,510,170],{"class":108},[91,512,514],{"class":93,"line":513},15,[91,515,516],{"class":108},"    })\n",[91,518,520,523,525,528,530,533],{"class":93,"line":519},16,[91,521,522],{"class":176},"    console",[91,524,180],{"class":108},[91,526,527],{"class":104},"log",[91,529,164],{"class":108},[91,531,532],{"class":200},"response",[91,534,170],{"class":108},[91,536,538],{"class":93,"line":537},17,[91,539,339],{"class":108},[18,541,542],{},"Finally, the cron handler and build status parsing:",[83,544,546],{"className":85,"code":545,"language":87,"meta":74,"style":74},"export default {\n    async fetch(request, env, ctx) {\n      return new Response('Hello World!');\n    },\n    async scheduled(event, env, ctx) {\n        const data = await fetchCopr()\n        const errorPackages = new Array()\n        for (const pkg of data.packages) {\n            for (const chroot of Object.values(pkg.chroots)) {\n                if (chroot.state == \"failed\") {\n                    errorPackages.push(pkg.name)\n                    break\n                }\n            }\n        }\n        if (errorPackages.length > 0) {\n            await notify(`COPR build failures detected for the following packages:\\n${errorPackages.join(\"\\n\")}`)\n        } else {\n            console.log(\"All COPR packages built successfully\")\n        }\n    }\n};\n",[65,547,548,559,583,601,606,628,643,659,684,719,742,763,768,773,778,782,806,854,865,882,887,892],{"__ignoreMap":74},[91,549,550,553,557],{"class":93,"line":94},[91,551,552],{"class":97},"export",[91,554,556],{"class":555},"sq3v1"," default",[91,558,388],{"class":108},[91,560,561,564,566,568,571,573,576,578,581],{"class":93,"line":112},[91,562,563],{"class":97},"    async",[91,565,244],{"class":104},[91,567,164],{"class":108},[91,569,570],{"class":361},"request",[91,572,197],{"class":108},[91,574,575],{"class":361},"env",[91,577,197],{"class":108},[91,579,580],{"class":361},"ctx",[91,582,300],{"class":108},[91,584,585,588,590,593,595,598],{"class":93,"line":133},[91,586,587],{"class":97},"      return",[91,589,158],{"class":97},[91,591,592],{"class":104}," Response",[91,594,164],{"class":108},[91,596,597],{"class":126},"'Hello World!'",[91,599,600],{"class":108},");\n",[91,602,603],{"class":93,"line":148},[91,604,605],{"class":108},"    },\n",[91,607,608,610,613,615,618,620,622,624,626],{"class":93,"line":173},[91,609,563],{"class":97},[91,611,612],{"class":104}," scheduled",[91,614,164],{"class":108},[91,616,617],{"class":361},"event",[91,619,197],{"class":108},[91,621,575],{"class":361},[91,623,197],{"class":108},[91,625,580],{"class":361},[91,627,300],{"class":108},[91,629,630,633,635,637,639,641],{"class":93,"line":206},[91,631,632],{"class":97},"        const",[91,634,259],{"class":118},[91,636,123],{"class":122},[91,638,241],{"class":97},[91,640,105],{"class":104},[91,642,273],{"class":108},[91,644,645,647,650,652,654,657],{"class":93,"line":231},[91,646,632],{"class":97},[91,648,649],{"class":118}," errorPackages",[91,651,123],{"class":122},[91,653,158],{"class":97},[91,655,656],{"class":104}," Array",[91,658,273],{"class":108},[91,660,661,664,666,669,672,675,677,679,682],{"class":93,"line":254},[91,662,663],{"class":97},"        for",[91,665,282],{"class":108},[91,667,668],{"class":97},"const",[91,670,671],{"class":118}," pkg",[91,673,674],{"class":97}," of",[91,676,259],{"class":176},[91,678,180],{"class":108},[91,680,681],{"class":290},"packages",[91,683,300],{"class":108},[91,685,686,689,691,693,696,698,701,703,706,708,711,713,716],{"class":93,"line":276},[91,687,688],{"class":97},"            for",[91,690,282],{"class":108},[91,692,668],{"class":97},[91,694,695],{"class":118}," chroot",[91,697,674],{"class":97},[91,699,700],{"class":176}," Object",[91,702,180],{"class":108},[91,704,705],{"class":104},"values",[91,707,164],{"class":108},[91,709,710],{"class":176},"pkg",[91,712,180],{"class":108},[91,714,715],{"class":290},"chroots",[91,717,718],{"class":108},")) {\n",[91,720,721,724,726,729,731,734,737,740],{"class":93,"line":303},[91,722,723],{"class":97},"                if",[91,725,282],{"class":108},[91,727,728],{"class":176},"chroot",[91,730,180],{"class":108},[91,732,733],{"class":290},"state",[91,735,736],{"class":122}," ==",[91,738,739],{"class":126}," \"failed\"",[91,741,300],{"class":108},[91,743,744,747,749,752,754,756,758,761],{"class":93,"line":321},[91,745,746],{"class":176},"                    errorPackages",[91,748,180],{"class":108},[91,750,751],{"class":104},"push",[91,753,164],{"class":108},[91,755,710],{"class":176},[91,757,180],{"class":108},[91,759,760],{"class":290},"name",[91,762,170],{"class":108},[91,764,765],{"class":93,"line":327},[91,766,767],{"class":97},"                    break\n",[91,769,770],{"class":93,"line":336},[91,771,772],{"class":108},"                }\n",[91,774,775],{"class":93,"line":490},[91,776,777],{"class":108},"            }\n",[91,779,780],{"class":93,"line":513},[91,781,427],{"class":108},[91,783,784,787,789,792,794,797,800,804],{"class":93,"line":519},[91,785,786],{"class":97},"        if",[91,788,282],{"class":108},[91,790,791],{"class":176},"errorPackages",[91,793,180],{"class":108},[91,795,796],{"class":290},"length",[91,798,799],{"class":122}," >",[91,801,803],{"class":802},"sAGMh"," 0",[91,805,300],{"class":108},[91,807,808,811,813,815,818,821,825,827,830,833,835,839,841,843,846,849,852],{"class":93,"line":537},[91,809,810],{"class":97},"            await",[91,812,356],{"class":104},[91,814,164],{"class":108},[91,816,817],{"class":126},"`COPR build failures detected for the following packages:",[91,819,820],{"class":122},"\\n",[91,822,824],{"class":823},"sAOjX","${",[91,826,791],{"class":176},[91,828,180],{"class":829},"sMj0N",[91,831,832],{"class":104},"join",[91,834,164],{"class":108},[91,836,838],{"class":837},"sWwuK","\"",[91,840,820],{"class":122},[91,842,838],{"class":837},[91,844,845],{"class":108},")",[91,847,848],{"class":823},"}",[91,850,851],{"class":126},"`",[91,853,170],{"class":108},[91,855,857,860,863],{"class":93,"line":856},18,[91,858,859],{"class":108},"        } ",[91,861,862],{"class":97},"else",[91,864,388],{"class":108},[91,866,868,871,873,875,877,880],{"class":93,"line":867},19,[91,869,870],{"class":176},"            console",[91,872,180],{"class":108},[91,874,527],{"class":104},[91,876,164],{"class":108},[91,878,879],{"class":126},"\"All COPR packages built successfully\"",[91,881,170],{"class":108},[91,883,885],{"class":93,"line":884},20,[91,886,427],{"class":108},[91,888,890],{"class":93,"line":889},21,[91,891,324],{"class":108},[91,893,895],{"class":93,"line":894},22,[91,896,897],{"class":108},"};\n",[18,899,900],{},"Then go to the Cloudflare Workers Settings page and configure a Cron expression. I set mine to trigger at minute 55 of every hour — that's only 24 Workers invocations per day, basically nothing.",[18,902,903],{},[72,904],{"alt":74,"src":905},"https://static.031130.xyz/uploads/2025/02/23/c38edfd637934.webp",[18,907,908,912,913,916],{},[909,910,911],"strong",{},"Drawback:"," I didn't bother setting up a persistent database to track which packages have already been flagged. This means once a package fails, you'll get a notification every hour until it's fixed. ",[21,914,915],{},"Feels like spam calls from an unknown number."," I'm not planning to fix this for now — maybe I'll just lower the cron frequency instead.",[918,919,920],"style",{},"html pre.shiki code .sLKXg, html code.shiki .sLKXg{--shiki-default:#A626A4;--shiki-dark:#C678DD}html pre.shiki code .sAdtL, html code.shiki .sAdtL{--shiki-default:#4078F2;--shiki-dark:#61AFEF}html pre.shiki code .s5ixo, html code.shiki .s5ixo{--shiki-default:#383A42;--shiki-dark:#ABB2BF}html pre.shiki code .sNmU0, html code.shiki .sNmU0{--shiki-default:#986801;--shiki-dark:#E5C07B}html pre.shiki code .s_Sar, html code.shiki .s_Sar{--shiki-default:#0184BC;--shiki-dark:#56B6C2}html pre.shiki code .sDhpE, html code.shiki .sDhpE{--shiki-default:#50A14F;--shiki-dark:#98C379}html pre.shiki code .s7GmK, html code.shiki .s7GmK{--shiki-default:#383A42;--shiki-dark:#E5C07B}html pre.shiki code .s2QsP, html code.shiki .s2QsP{--shiki-default:#E45649;--shiki-dark:#E5C07B}html pre.shiki code .sz0mV, html code.shiki .sz0mV{--shiki-default:#383A42;--shiki-dark:#E06C75}html pre.shiki code .sJa8x, html code.shiki .sJa8x{--shiki-default:#E45649;--shiki-dark:#E06C75}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s8iYz, html code.shiki .s8iYz{--shiki-default:#383A42;--shiki-default-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic}html pre.shiki code .st7oF, html code.shiki .st7oF{--shiki-default:#0184BC;--shiki-dark:#ABB2BF}html pre.shiki code .sq3v1, html code.shiki .sq3v1{--shiki-default:#E45649;--shiki-dark:#C678DD}html pre.shiki code .sAGMh, html code.shiki .sAGMh{--shiki-default:#986801;--shiki-dark:#D19A66}html pre.shiki code .sAOjX, html code.shiki .sAOjX{--shiki-default:#CA1243;--shiki-dark:#C678DD}html pre.shiki code .sMj0N, html code.shiki .sMj0N{--shiki-default:#50A14F;--shiki-dark:#ABB2BF}html pre.shiki code .sWwuK, html code.shiki .sWwuK{--shiki-default:#CA1243;--shiki-dark:#98C379}",{"title":74,"searchDepth":112,"depth":112,"links":922},[],[924,930],{"title":925,"path":926,"stem":927,"date":928,"lang":929,"children":-1},"Cudy TR3000 daed Installation Guide","/2025/02/28/cudy-tr3000-daed-install-record","posts/en/cudy-tr3000-daed-install-record","2025-02-28 21:18:34","en",null,"/2025/02/23/monitor-copr-build-state-with-cloudflare-workers/",1782585025537]