mirror of
https://github.com/Xevion/xevion.dev.git
synced 2025-12-05 23:16:57 -06:00
automatic update cron implementation, vercel cron, type guards
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@kodingdotninja/use-tailwind-breakpoint": "^1.0.0",
|
"@kodingdotninja/use-tailwind-breakpoint": "^1.0.0",
|
||||||
"@next/eslint-plugin-next": "^15.1.1",
|
"@next/eslint-plugin-next": "^15.1.1",
|
||||||
|
"@octokit/core": "^6.1.2",
|
||||||
"@plaiceholder/next": "^3.0.0",
|
"@plaiceholder/next": "^3.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.8",
|
"@tailwindcss/typography": "^0.5.8",
|
||||||
"@tanstack/react-query": "^4.16.0",
|
"@tanstack/react-query": "^4.16.0",
|
||||||
|
|||||||
93
pnpm-lock.yaml
generated
93
pnpm-lock.yaml
generated
@@ -20,6 +20,9 @@ importers:
|
|||||||
'@next/eslint-plugin-next':
|
'@next/eslint-plugin-next':
|
||||||
specifier: ^15.1.1
|
specifier: ^15.1.1
|
||||||
version: 15.1.1
|
version: 15.1.1
|
||||||
|
'@octokit/core':
|
||||||
|
specifier: ^6.1.2
|
||||||
|
version: 6.1.2
|
||||||
'@plaiceholder/next':
|
'@plaiceholder/next':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0(next@15.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.56.2))(plaiceholder@3.0.0(sharp@0.32.1))(sharp@0.32.1)
|
version: 3.0.0(next@15.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.56.2))(plaiceholder@3.0.0(sharp@0.32.1))(sharp@0.32.1)
|
||||||
@@ -433,6 +436,36 @@ packages:
|
|||||||
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
||||||
engines: {node: '>=12.4.0'}
|
engines: {node: '>=12.4.0'}
|
||||||
|
|
||||||
|
'@octokit/auth-token@5.1.1':
|
||||||
|
resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
'@octokit/core@6.1.2':
|
||||||
|
resolution: {integrity: sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
'@octokit/endpoint@10.1.2':
|
||||||
|
resolution: {integrity: sha512-XybpFv9Ms4hX5OCHMZqyODYqGTZ3H6K6Vva+M9LR7ib/xr1y1ZnlChYv9H680y77Vd/i/k+thXApeRASBQkzhA==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
'@octokit/graphql@8.1.2':
|
||||||
|
resolution: {integrity: sha512-bdlj/CJVjpaz06NBpfHhp4kGJaRZfz7AzC+6EwUImRtrwIw8dIgJ63Xg0OzV9pRn3rIzrt5c2sa++BL0JJ8GLw==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
'@octokit/openapi-types@22.2.0':
|
||||||
|
resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==}
|
||||||
|
|
||||||
|
'@octokit/request-error@6.1.6':
|
||||||
|
resolution: {integrity: sha512-pqnVKYo/at0NuOjinrgcQYpEbv4snvP3bKMRqHaD9kIsk9u1LCpb2smHZi8/qJfgeNqLo5hNW4Z7FezNdEo0xg==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
'@octokit/request@9.1.4':
|
||||||
|
resolution: {integrity: sha512-tMbOwGm6wDII6vygP3wUVqFTw3Aoo0FnVQyhihh8vVq12uO3P+vQZeo2CKMpWtPSogpACD0yyZAlVlQnjW71DA==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
'@octokit/types@13.6.2':
|
||||||
|
resolution: {integrity: sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -758,6 +791,9 @@ packages:
|
|||||||
base64-js@1.5.1:
|
base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
|
before-after-hook@3.0.2:
|
||||||
|
resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==}
|
||||||
|
|
||||||
binary-extensions@2.2.0:
|
binary-extensions@2.2.0:
|
||||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1146,6 +1182,9 @@ packages:
|
|||||||
extend@3.0.2:
|
extend@3.0.2:
|
||||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||||
|
|
||||||
|
fast-content-type-parse@2.0.0:
|
||||||
|
resolution: {integrity: sha512-fCqg/6Sps8tqk8p+kqyKqYfOF0VjPNYrqpLiqNl0RBKmD80B080AJWVV6EkSkscjToNExcXg1+Mfzftrx6+iSA==}
|
||||||
|
|
||||||
fast-deep-equal@3.1.3:
|
fast-deep-equal@3.1.3:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
@@ -2338,6 +2377,9 @@ packages:
|
|||||||
unist-util-visit@5.0.0:
|
unist-util-visit@5.0.0:
|
||||||
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
|
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
|
||||||
|
|
||||||
|
universal-user-agent@7.0.2:
|
||||||
|
resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==}
|
||||||
|
|
||||||
update-browserslist-db@1.0.10:
|
update-browserslist-db@1.0.10:
|
||||||
resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
|
resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -2667,6 +2709,47 @@ snapshots:
|
|||||||
|
|
||||||
'@nolyfill/is-core-module@1.0.39': {}
|
'@nolyfill/is-core-module@1.0.39': {}
|
||||||
|
|
||||||
|
'@octokit/auth-token@5.1.1': {}
|
||||||
|
|
||||||
|
'@octokit/core@6.1.2':
|
||||||
|
dependencies:
|
||||||
|
'@octokit/auth-token': 5.1.1
|
||||||
|
'@octokit/graphql': 8.1.2
|
||||||
|
'@octokit/request': 9.1.4
|
||||||
|
'@octokit/request-error': 6.1.6
|
||||||
|
'@octokit/types': 13.6.2
|
||||||
|
before-after-hook: 3.0.2
|
||||||
|
universal-user-agent: 7.0.2
|
||||||
|
|
||||||
|
'@octokit/endpoint@10.1.2':
|
||||||
|
dependencies:
|
||||||
|
'@octokit/types': 13.6.2
|
||||||
|
universal-user-agent: 7.0.2
|
||||||
|
|
||||||
|
'@octokit/graphql@8.1.2':
|
||||||
|
dependencies:
|
||||||
|
'@octokit/request': 9.1.4
|
||||||
|
'@octokit/types': 13.6.2
|
||||||
|
universal-user-agent: 7.0.2
|
||||||
|
|
||||||
|
'@octokit/openapi-types@22.2.0': {}
|
||||||
|
|
||||||
|
'@octokit/request-error@6.1.6':
|
||||||
|
dependencies:
|
||||||
|
'@octokit/types': 13.6.2
|
||||||
|
|
||||||
|
'@octokit/request@9.1.4':
|
||||||
|
dependencies:
|
||||||
|
'@octokit/endpoint': 10.1.2
|
||||||
|
'@octokit/request-error': 6.1.6
|
||||||
|
'@octokit/types': 13.6.2
|
||||||
|
fast-content-type-parse: 2.0.0
|
||||||
|
universal-user-agent: 7.0.2
|
||||||
|
|
||||||
|
'@octokit/types@13.6.2':
|
||||||
|
dependencies:
|
||||||
|
'@octokit/openapi-types': 22.2.0
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -3034,6 +3117,8 @@ snapshots:
|
|||||||
|
|
||||||
base64-js@1.5.1: {}
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
|
before-after-hook@3.0.2: {}
|
||||||
|
|
||||||
binary-extensions@2.2.0: {}
|
binary-extensions@2.2.0: {}
|
||||||
|
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
@@ -3395,7 +3480,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.18.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@9.17.0(jiti@1.21.7)))(eslint@9.17.0(jiti@1.21.7)):
|
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.18.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.17.0(jiti@1.21.7)):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -3417,7 +3502,7 @@ snapshots:
|
|||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 9.17.0(jiti@1.21.7)
|
eslint: 9.17.0(jiti@1.21.7)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.18.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@9.17.0(jiti@1.21.7)))(eslint@9.17.0(jiti@1.21.7))
|
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.18.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.17.0(jiti@1.21.7))
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.0
|
is-core-module: 2.16.0
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -3554,6 +3639,8 @@ snapshots:
|
|||||||
|
|
||||||
extend@3.0.2: {}
|
extend@3.0.2: {}
|
||||||
|
|
||||||
|
fast-content-type-parse@2.0.0: {}
|
||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
fast-glob@3.3.1:
|
fast-glob@3.3.1:
|
||||||
@@ -4998,6 +5085,8 @@ snapshots:
|
|||||||
unist-util-is: 6.0.0
|
unist-util-is: 6.0.0
|
||||||
unist-util-visit-parents: 6.0.1
|
unist-util-visit-parents: 6.0.1
|
||||||
|
|
||||||
|
universal-user-agent@7.0.2: {}
|
||||||
|
|
||||||
update-browserslist-db@1.0.10(browserslist@4.21.4):
|
update-browserslist-db@1.0.10(browserslist@4.21.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.21.4
|
browserslist: 4.21.4
|
||||||
|
|||||||
315
src/pages/api/cron/updated.ts
Normal file
315
src/pages/api/cron/updated.ts
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
/**
|
||||||
|
* This is a cron job handler for acquiring the latest 'updated' data for the site's projects.
|
||||||
|
*
|
||||||
|
* 1) Fetch the list of all projects including their link URLs.
|
||||||
|
* 2) Filter the list only for projects with 'autocheck_update' enabled and any 'github.com' link.
|
||||||
|
* 3) For each project, query the GitHub API for the latest commit date on all branches.
|
||||||
|
* 4) If the latest commit date is newer than the project's 'last_updated' date, update the project's 'last_updated' date.
|
||||||
|
* 5) If any project's 'last_updated' date was updated, revalidate the index and/or project page.
|
||||||
|
* 6) Report the results of this cron job invocation.
|
||||||
|
*
|
||||||
|
* - This cron job runs at least once a day, at most once an hour.
|
||||||
|
* - This cron job is completely asynchronous but respects GitHub API rate limits.
|
||||||
|
* - This cron job requires authentication with the Directus API.
|
||||||
|
* - This cron job requires authentication with the GitHub API (mostly for rate limits).
|
||||||
|
*/
|
||||||
|
import directus, { ProjectLink } from "@/utils/directus";
|
||||||
|
import { readItems, updateItem } from "@directus/sdk";
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { Octokit } from "@octokit/core";
|
||||||
|
import { isFulfilled, isRejected } from "@/utils/types";
|
||||||
|
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: process.env.GITHUB_API_TOKEN,
|
||||||
|
request: {
|
||||||
|
fetch: (url: string | URL, options: RequestInit) => {
|
||||||
|
console.log(`${options.method} ${url}`);
|
||||||
|
return fetch(url, options);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type ProjectResult = {
|
||||||
|
id: string;
|
||||||
|
previousUpdated: Date | null;
|
||||||
|
latestUpdated: Date | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRepository(url: string): [string, string] | null {
|
||||||
|
const pattern = /github.com\/([^/]+)\/([^/]+)/;
|
||||||
|
const match = pattern.exec(url);
|
||||||
|
|
||||||
|
if (match === null) return null;
|
||||||
|
return [match[1]!, match[2]!];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleProject({
|
||||||
|
id: project_id,
|
||||||
|
urls,
|
||||||
|
date_updated: previousUpdated,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
urls: string[];
|
||||||
|
date_updated: Date | null;
|
||||||
|
}): Promise<ProjectResult> {
|
||||||
|
// Extract the branches from each URL
|
||||||
|
const allBranches = await Promise.all(
|
||||||
|
urls.map(async (url) => {
|
||||||
|
const details = getRepository(url);
|
||||||
|
if (!details) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle deduplication of repository targets
|
||||||
|
const [owner, repo] = details;
|
||||||
|
const branches = await octokit.request(
|
||||||
|
"GET /repos/{owner}/{repo}/branches",
|
||||||
|
{
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
headers: {
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return branches.data.map((branch) => ({
|
||||||
|
branch: branch.name,
|
||||||
|
owner: owner,
|
||||||
|
repo: repo,
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the latest commit date for each branch (flattened)
|
||||||
|
const latestCommits = allBranches
|
||||||
|
.flat()
|
||||||
|
.map(async ({ owner, repo, branch }) => {
|
||||||
|
const commits = await octokit.request(
|
||||||
|
"GET /repos/{owner}/{repo}/commits",
|
||||||
|
{
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
sha: branch,
|
||||||
|
per_page: 1,
|
||||||
|
headers: {
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const latestCommit = commits.data[0];
|
||||||
|
|
||||||
|
// Commits not returned
|
||||||
|
if (latestCommit == null) {
|
||||||
|
console.warn({
|
||||||
|
target: `${owner}/${repo}@${branch}`,
|
||||||
|
message: "No commits available",
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle missing commit data in unpredictable cases
|
||||||
|
if (latestCommit.commit.author == null) {
|
||||||
|
console.warn({
|
||||||
|
target: `${owner}/${repo}@${branch}`,
|
||||||
|
sha: latestCommit.sha,
|
||||||
|
commit: latestCommit.commit.message,
|
||||||
|
url: latestCommit.html_url,
|
||||||
|
message: "No author available",
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
} else if (latestCommit.commit.author.date == null) {
|
||||||
|
console.warn({
|
||||||
|
target: `${owner}/${repo}@${branch}`,
|
||||||
|
sha: latestCommit.sha,
|
||||||
|
commit: latestCommit.commit.message,
|
||||||
|
url: latestCommit.html_url,
|
||||||
|
message: "No date available",
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(latestCommit.commit.author.date);
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(latestCommits);
|
||||||
|
|
||||||
|
// Handle the promises that failed
|
||||||
|
results.filter(isRejected).forEach((result) => {
|
||||||
|
// TODO: Add more context to the error message
|
||||||
|
console.error("Failed to fetch latest commit date", result.reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the latest commit date
|
||||||
|
const latestUpdated = results
|
||||||
|
.filter(isFulfilled)
|
||||||
|
.map((v) => v.value)
|
||||||
|
.filter((v) => v != null)
|
||||||
|
.reduce((previous: Date | null, current: Date) => {
|
||||||
|
if (previous == null) return current;
|
||||||
|
return current > previous ? current : previous;
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
if (latestUpdated == null) {
|
||||||
|
console.error("Unable to acquire the latest commit date for project");
|
||||||
|
return {
|
||||||
|
id: project_id,
|
||||||
|
previousUpdated,
|
||||||
|
latestUpdated: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure it's a reasonable date
|
||||||
|
if (latestUpdated != null && latestUpdated < new Date("2015-01-01")) {
|
||||||
|
console.error("Invalid commit date acquired", latestUpdated);
|
||||||
|
return {
|
||||||
|
id: project_id,
|
||||||
|
previousUpdated,
|
||||||
|
latestUpdated: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = { id: project_id, previousUpdated, latestUpdated: null };
|
||||||
|
|
||||||
|
// Update the project's 'last_updated' date if the latest commit date is newer
|
||||||
|
if (previousUpdated == null || latestUpdated > previousUpdated) {
|
||||||
|
await directus.request(
|
||||||
|
updateItem("project", project_id, {
|
||||||
|
date_updated: latestUpdated,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 'latestUpdated' is not null ONLY if the project was actually updated
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
latestUpdated,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
// Check for the required environment variables
|
||||||
|
const { CRON_SECRET, GITHUB_API_TOKEN, DIRECTUS_API_TOKEN } = process.env;
|
||||||
|
if (!CRON_SECRET || !GITHUB_API_TOKEN || !DIRECTUS_API_TOKEN) {
|
||||||
|
res.status(500).json({ error: "Missing environment variables" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the cron request is authenticated
|
||||||
|
if (process.env.NODE_ENV !== "development") {
|
||||||
|
const authHeader = req.headers["authorization"];
|
||||||
|
if (authHeader !== `Bearer ${CRON_SECRET}`) {
|
||||||
|
return new Response("Unauthorized", {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let request_count = 0;
|
||||||
|
octokit.hook.before("request", async () => {
|
||||||
|
request_count++;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch the list of all projects including their link URLs.
|
||||||
|
const projects = await directus.request(
|
||||||
|
readItems("project", {
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"autocheckUpdated",
|
||||||
|
"date_updated",
|
||||||
|
{ links: ["url"] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter the list only for projects with 'autocheck_update' enabled and any 'github.com' link.
|
||||||
|
const eligibleProjects = projects
|
||||||
|
.map((project) => {
|
||||||
|
// Skip projects that don't have autocheckUpdated enabled.
|
||||||
|
if (!project.autocheckUpdated) return null;
|
||||||
|
|
||||||
|
// Acquire the URL from the link, then filter out any non-GitHub URLs.
|
||||||
|
const urls = project
|
||||||
|
.links!.map((link) => {
|
||||||
|
return (<ProjectLink>link).url;
|
||||||
|
})
|
||||||
|
.filter((url) => url.includes("github.com"));
|
||||||
|
|
||||||
|
// Skip projects that don't have any GitHub URLs.
|
||||||
|
if (urls.length === 0) return null;
|
||||||
|
|
||||||
|
// Return the project's most important data for further processing.
|
||||||
|
return {
|
||||||
|
id: project.id,
|
||||||
|
name: project.name,
|
||||||
|
date_updated: project.date_updated,
|
||||||
|
urls,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
// null values are still included in the array, so filter them out.
|
||||||
|
.filter((project) => project !== null);
|
||||||
|
|
||||||
|
// Log the date_updated for each project
|
||||||
|
eligibleProjects.forEach((project) => {
|
||||||
|
console.log({
|
||||||
|
name: project.name,
|
||||||
|
date_updated: project.date_updated,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// For each project, query the GitHub API for the latest commit date on all branches.
|
||||||
|
const projectPromises = eligibleProjects.map((project) =>
|
||||||
|
handleProject({
|
||||||
|
id: project.id,
|
||||||
|
urls: project.urls,
|
||||||
|
date_updated:
|
||||||
|
project.date_updated != null ? new Date(project.date_updated) : null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for all project promises to resolve
|
||||||
|
const results = await Promise.allSettled(projectPromises);
|
||||||
|
|
||||||
|
// If more than 10% of the requests failed, return an error status code
|
||||||
|
const isFailed = results.filter(isRejected).length > results.length * 0.1;
|
||||||
|
|
||||||
|
type Response = {
|
||||||
|
request_count: number;
|
||||||
|
errors: { project_name: string; reason: string }[];
|
||||||
|
ignored: string[];
|
||||||
|
changed: { project_name: string; previous: Date | null; latest: Date }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const fulfilled = results.filter(isFulfilled);
|
||||||
|
|
||||||
|
const response: Response = {
|
||||||
|
request_count,
|
||||||
|
errors: results.filter(isRejected).map((r) => ({
|
||||||
|
// TODO: Fix this project name
|
||||||
|
project_name: "unknown",
|
||||||
|
reason: r.reason,
|
||||||
|
})),
|
||||||
|
ignored: fulfilled
|
||||||
|
.filter((r) => r.value.latestUpdated == null)
|
||||||
|
.map((r) => r.value.id),
|
||||||
|
changed: fulfilled
|
||||||
|
.filter((r) => r.value.latestUpdated != null)
|
||||||
|
.map((r) => ({
|
||||||
|
project_name: r.value.id,
|
||||||
|
previous: r.value.previousUpdated,
|
||||||
|
latest: r.value.latestUpdated!,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(!isFailed ? 200 : 500).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,7 +71,14 @@ export interface Metadata {
|
|||||||
resumeFilename: string;
|
resumeFilename: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const directus = createDirectus<Schema>("https://api.xevion.dev")
|
const directus = createDirectus<Schema>("https://api.xevion.dev", {
|
||||||
|
globals: {
|
||||||
|
fetch: (input, init) => {
|
||||||
|
console.log(`${init.method?.toUpperCase()} ${input}`);
|
||||||
|
return fetch(input, init);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
.with(staticToken(env.DIRECTUS_API_TOKEN))
|
.with(staticToken(env.DIRECTUS_API_TOKEN))
|
||||||
.with(rest());
|
.with(rest());
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ import type { IconType } from "react-icons";
|
|||||||
import { AiFillGithub, AiOutlineLink } from "react-icons/ai";
|
import { AiFillGithub, AiOutlineLink } from "react-icons/ai";
|
||||||
import { RxOpenInNewWindow } from "react-icons/rx";
|
import { RxOpenInNewWindow } from "react-icons/rx";
|
||||||
|
|
||||||
export type Project = {
|
// Promise.allSettled type guards
|
||||||
title: string;
|
export const isFulfilled = <T>(
|
||||||
banner: string;
|
p: PromiseSettledResult<T>,
|
||||||
bannerSettings?: { quality: number };
|
): p is PromiseFulfilledResult<T> => p.status === "fulfilled";
|
||||||
longDescription: string;
|
export const isRejected = <T>(
|
||||||
shortDescription: string;
|
p: PromiseSettledResult<T>,
|
||||||
links?: LinkIcon[];
|
): p is PromiseRejectedResult => p.status === "rejected";
|
||||||
location: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LinkIcons: Record<string, IconType> = {
|
export const LinkIcons: Record<string, IconType> = {
|
||||||
github: AiFillGithub,
|
github: AiFillGithub,
|
||||||
|
|||||||
8
vercel.json
Normal file
8
vercel.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"crons": [
|
||||||
|
{
|
||||||
|
"path": "/api/cron/updated",
|
||||||
|
"schedule": "7 */2 * * *"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user