diff --git a/.github/scripts/gen-index.html.ts b/.github/scripts/gen-index.html.ts new file mode 100755 index 0000000..75db11d --- /dev/null +++ b/.github/scripts/gen-index.html.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env -S pkgx deno run --allow-read --allow-write=./out + +console.log('hio') + +interface Pkg { + name?: string + project: string + description: string +} + +const pkgs: Pkg[] = JSON.parse(Deno.readTextFileSync("./out/index.json")); + +if (pkgs.length <= 0) { + throw new Error("Empty pkgs!") +} + +for (const pkg of pkgs) { + Deno.mkdirSync(`./out/${pkg.project}`, {recursive: true}); + + const title = `${pkg.name || pkg.project} — pkgx`; + + let txt = Deno.readTextFileSync('./out/index.html'); + txt = replace(txt, 'title', title); + txt = replace(txt, 'description', pkg.description); + txt = replace(txt, 'image', `https://gui.tea.xyz/prod/${pkg.project}/1024x1024.webp`); + txt = replace(txt, 'url', `https://pkgx.dev/pkgs/${pkg.project}/`); + + txt = txt.replace(/.*<\/title>/, `<title>${title}`); + + Deno.writeTextFileSync(`./out/${pkg.project}/index.html`, txt); + + console.log(`./out/${pkg.project}/index.html`) +} + +function replace(txt: string, attr: string, value: string) { + return txt.replace(new RegExp(``), ``) +} diff --git a/.github/scripts/gen-index.json.ts b/.github/scripts/gen-index.json.ts new file mode 100755 index 0000000..7e74149 --- /dev/null +++ b/.github/scripts/gen-index.json.ts @@ -0,0 +1,134 @@ +#!/usr/bin/env -S pkgx deno run -A + +import * as fs from "node:fs" + +interface Package { + project: string, + birthtime: Date, + name?: string + description?: string + labels?: string[] +} + +console.error('HI') + +export async function getKettleRemoteMetadata() { + const headers = { Authorization: 'public' } + const rsp = await fetch(`https://app.pkgx.dev/v1/packages/`, {headers}) + const foo = await rsp.json() as {project: string, short_description: string}[] + return foo.reduce((acc, {project, short_description}) => { + acc[project] = short_description + return acc + }, {} as Record) +} + +const descriptions = await getKettleRemoteMetadata(); + +async function getPackageYmlCreationDates(): Promise { + const cmdString = "git log --pretty=format:'%H %aI' --name-only --diff-filter=A -- 'projects/**/package.yml'"; + const process = Deno.run({ + cmd: ["bash", "-c", cmdString], + stdout: "piped", + }); + + const output = new TextDecoder().decode(await process.output()); + await process.status(); + process.close(); + + const lines = output.trim().split('\n'); + const rv: Package[] = [] + let currentCommitDate: string | null = null; + + for (const line of lines) { + if (line.includes(' ')) { // Detect lines with commit hash and date + currentCommitDate = line.split(' ')[1]; + } else if (line.endsWith('package.yml')) { + const project = line.slice(9, -12) + + if (!fs.existsSync(line)) { + // the file used to exist but has been deleted + console.warn("skipping yanked: ", project) + continue + } + + const birthtime = new Date(currentCommitDate!) + const name = await get_name(line, project) + + let description: string | undefined = descriptions[project]?.trim() + if (!description) description = undefined + + let labels: string[] | undefined = [...await get_labels(line)] + if (labels.length == 0) labels = undefined + + rv.push({ project, birthtime, name, description, labels }) + } + } + + return rv; +} + +const pkgs = await getPackageYmlCreationDates(); + +console.error({pkgs}) + +// sort by birthtime +pkgs.sort((a, b) => b.birthtime.getTime() - a.birthtime.getTime()); + +console.log(JSON.stringify(pkgs, null, 2)); + +////////////////////////////////////////////////////// +import { parse } from "https://deno.land/std@0.204.0/yaml/mod.ts"; +import { isArray } from "https://deno.land/x/is_what@v4.1.15/src/index.ts"; +import get_pkg_name from "../../src/utils/pkg-name.ts"; + +async function get_name(path: string, project: string): Promise { + const txt = await Deno.readTextFileSync(path) + const yml = await parse(txt) as Record + if (yml['display-name']) { + return yml['display-name'] + } else if (isArray(yml.provides) && yml.provides.length == 1) { + return yml.provides[0].slice(4) + } else { + return get_pkg_name(project) + } +} + +import { parse_pkgs_node } from "https://deno.land/x/libpkgx@v0.15.1/src/hooks/usePantry.ts" + +async function get_labels(path: string) { + const txt = await Deno.readTextFileSync(path) + const yml = await parse(txt) as Record + const deps = parse_pkgs_node(yml.dependencies) //NOTE will ignore other platforms than linux, but should be fine + + const labels = new Set(deps.compact(({ project }) => { + switch (project) { + case 'nodejs.org': + case 'npmjs.com': + return 'node' + case 'python.org': + case 'pip.pypa.io': + return 'python' + case 'ruby-lang.org': + case 'rubygems.org': + return 'ruby' + case 'rust-lang.org': + case 'rust-lang.org/cargo': + return 'rust' + } + })) + + console.error(path) + + if (yml.build?.dependencies) for (const dep of parse_pkgs_node(yml.build.dependencies)) { + switch (dep.project) { + case 'rust-lang.org': + case 'rust-lang.org/cargo': + labels.add('rust') + break + case 'go.dev': + labels.add('go') + } + } + + return labels +} diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9962663..5ee6232 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -110,3 +110,8 @@ jobs: - uses: pkgxdev/setup@v1 - run: pkgx hugo - run: pkgx hugo deploy + + indexer: + needs: publish + uses: ./.github/workflows/indexer.yml + secrets: inherit diff --git a/.github/workflows/indexer.yml b/.github/workflows/indexer.yml new file mode 100644 index 0000000..1469252 --- /dev/null +++ b/.github/workflows/indexer.yml @@ -0,0 +1,62 @@ +name: indexer + +# we have to generate an index for each pkg so that the index.html that is +# served has the correct metadata + +#TODO a lambda would be better because we copy the index.html from the bucket +# root and modify it then push that back so it could easily get out of sync + +on: + workflow_call: + workflow_dispatch: + schedule: + - cron: "13,26,39,52 3,9,15,21 * * *" + pull_request: + branches: main + paths: + - .github/scripts/indexer.ts + - .github/workflows/indexer.yml + +concurrency: + group: indexer/${{ github.ref || 'main' }} + cancel-in-progress: true + +jobs: + indexer: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/checkout@v4 + with: + repository: pkgxdev/pantry + path: pantry + fetch-depth: 0 # needed to get git metadata for ordering purposes + + - uses: pkgxdev/setup@v2 + + - uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.WWW_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.WWW_AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-2 + + - run: aws s3 sync s3://www.pkgx.dev/pkgs/ ./out + - run: aws s3 cp s3://www.pkgx.dev/index.html ./out/index.html + + - run: ../.github/scripts/gen-index.json.ts > ../out/index.json + working-directory: pantry + + - run: cat ./out/index.html + - run: .github/scripts/gen-index.html.ts + + - run: aws s3 sync + out/ s3://www.pkgx.dev/pkgs/ + --size-only + --metadata-directive REPLACE + --cache-control no-cache,must-revalidate + + - run: aws cloudfront + create-invalidation + --distribution-id E15VQ3SI584CSG + --paths /pkgs /pkgs/ /pkgs/* diff --git a/.gitignore b/.gitignore index 0771922..72637b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -/dist \ No newline at end of file +/dist +/out diff --git a/index.html b/index.html index 66bb00c..df69334 100644 --- a/index.html +++ b/index.html @@ -2,6 +2,7 @@ + pkgx — Run Anything @@ -11,11 +12,11 @@ - pkgx — Run Anything + - +
diff --git a/src/pkgx.dev/PackageShowcase.tsx b/src/pkgx.dev/PackageShowcase.tsx index 1fd7741..4df1094 100644 --- a/src/pkgx.dev/PackageShowcase.tsx +++ b/src/pkgx.dev/PackageShowcase.tsx @@ -5,7 +5,7 @@ import { Alert, Skeleton } from "@mui/material"; export default function showcase() { const {loading, error, value: pkgs} = useAsync(async () => { - const rsp = await fetch('https://pkgxdev.github.io/pantry/pkgs.json') + const rsp = await fetch('https://pkgx.dev/pkgs/index.json') return await rsp.json() })