mirror of
https://github.com/homarr-labs/dashboard-icons.git
synced 2026-01-12 16:25:38 +08:00
add workflow to add an icon
This commit is contained in:
91
.github/workflows/add-icon.yml
vendored
Normal file
91
.github/workflows/add-icon.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: Add Icon to Collection
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
submissionId:
|
||||
description: ID of the PocketBase submission to import
|
||||
required: true
|
||||
type: string
|
||||
dryRun:
|
||||
description: When true, skip writes/commit/PocketBase status update
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
secrets:
|
||||
PB_URL:
|
||||
required: true
|
||||
PB_ADMIN_TOKEN:
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
submissionId:
|
||||
description: ID of the PocketBase submission to import
|
||||
required: true
|
||||
type: string
|
||||
dryRun:
|
||||
description: When true, skip writes/commit/PocketBase status update
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
add-icon:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
|
||||
- name: Import icon from PocketBase submission
|
||||
id: import
|
||||
env:
|
||||
PB_URL: ${{ secrets.PB_URL }}
|
||||
PB_ADMIN_TOKEN: ${{ secrets.PB_ADMIN_TOKEN }}
|
||||
run: |
|
||||
ARGS=(--submission-id "${{ inputs.submissionId }}")
|
||||
if [ "${{ inputs.dryRun }}" = "true" ]; then
|
||||
ARGS+=("--dry-run")
|
||||
fi
|
||||
ARGS+=("--gha-output" "$GITHUB_OUTPUT")
|
||||
bun run scripts/import-icon.ts "${ARGS[@]}"
|
||||
|
||||
- name: Check for changes
|
||||
id: git-diff
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Configure git user
|
||||
if: ${{ !inputs.dryRun && steps.git-diff.outputs.has_changes == 'true' }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Commit changes
|
||||
if: ${{ !inputs.dryRun && steps.git-diff.outputs.has_changes == 'true' }}
|
||||
run: |
|
||||
submission_name="${{ steps.import.outputs.submission_name }}"
|
||||
if [ -z "$submission_name" ]; then
|
||||
submission_name="${{ inputs.submissionId }}"
|
||||
fi
|
||||
approver="${{ steps.import.outputs.approver }}"
|
||||
if [ -z "$approver" ]; then
|
||||
approver="unknown"
|
||||
fi
|
||||
git add .
|
||||
git commit -m "chore: add icon \"${submission_name}\" (submission ${{ inputs.submissionId }}, approved by ${approver})"
|
||||
|
||||
- name: Push changes
|
||||
if: ${{ !inputs.dryRun && steps.git-diff.outputs.has_changes == 'true' }}
|
||||
run: git push
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -32,4 +32,6 @@ Temporary Items
|
||||
*.icloud
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
__pycache__/
|
||||
|
||||
node_modules/
|
||||
42
docs/add-icon-workflow.md
Normal file
42
docs/add-icon-workflow.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Add Icon Workflow Usage
|
||||
|
||||
This repository exposes `.github/workflows/add-icon.yml` to import an accepted PocketBase submission into the collection, commit assets/metadata, and mark the submission as `added_to_collection`.
|
||||
|
||||
## Required secrets
|
||||
- `PB_URL`: PocketBase base URL (e.g., `https://pb.example.com`).
|
||||
- `PB_ADMIN_TOKEN`: PocketBase superuser/impersonation token with permission to read submissions and update their status.
|
||||
|
||||
## Inputs
|
||||
- `submissionId` (string, required): PocketBase submission record ID.
|
||||
- `dryRun` (boolean, optional, default `false`): Skip writes, commit, and status update.
|
||||
|
||||
## Trigger via GitHub API (backend/webhook)
|
||||
Use a token with `workflow` scope:
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
https://api.github.com/repos/<OWNER>/<REPO>/actions/workflows/add-icon.yml/dispatches \
|
||||
-d '{
|
||||
"ref": "main",
|
||||
"inputs": {
|
||||
"submissionId": "SUBMISSION_RECORD_ID",
|
||||
"dryRun": "false"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Trigger from another workflow
|
||||
```yaml
|
||||
jobs:
|
||||
add-icon:
|
||||
uses: ./.github/workflows/add-icon.yml
|
||||
with:
|
||||
submissionId: ${{ github.event.inputs.submissionId }}
|
||||
dryRun: false
|
||||
secrets:
|
||||
PB_URL: ${{ secrets.PB_URL }}
|
||||
PB_ADMIN_TOKEN: ${{ secrets.PB_ADMIN_TOKEN }}
|
||||
```
|
||||
|
||||
|
||||
6
package.json
Normal file
6
package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.5",
|
||||
"@types/node": "^25.0.3"
|
||||
}
|
||||
}
|
||||
46
pnpm-lock.yaml
generated
Normal file
46
pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,46 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@types/bun':
|
||||
specifier: ^1.3.5
|
||||
version: 1.3.5
|
||||
'@types/node':
|
||||
specifier: ^25.0.3
|
||||
version: 25.0.3
|
||||
|
||||
packages:
|
||||
|
||||
'@types/bun@1.3.5':
|
||||
resolution: {integrity: sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==}
|
||||
|
||||
'@types/node@25.0.3':
|
||||
resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==}
|
||||
|
||||
bun-types@1.3.5:
|
||||
resolution: {integrity: sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw==}
|
||||
|
||||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@types/bun@1.3.5':
|
||||
dependencies:
|
||||
bun-types: 1.3.5
|
||||
|
||||
'@types/node@25.0.3':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
|
||||
bun-types@1.3.5:
|
||||
dependencies:
|
||||
'@types/node': 25.0.3
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
470
scripts/import-icon.ts
Normal file
470
scripts/import-icon.ts
Normal file
@@ -0,0 +1,470 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { mkdir } from "node:fs/promises";
|
||||
|
||||
import path from "node:path";
|
||||
|
||||
type IconColors = { light?: string; dark?: string };
|
||||
type IconWordmark = { light?: string; dark?: string };
|
||||
|
||||
interface PBUser {
|
||||
id: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
interface SubmissionExtras {
|
||||
aliases?: string[];
|
||||
categories?: string[];
|
||||
base?: string;
|
||||
colors?: IconColors;
|
||||
wordmark?: IconWordmark;
|
||||
}
|
||||
|
||||
interface Submission {
|
||||
id: string;
|
||||
name: string;
|
||||
assets: string[];
|
||||
created_by: string;
|
||||
status: string;
|
||||
extras?: SubmissionExtras;
|
||||
approved_by?: string;
|
||||
description?: string;
|
||||
admin_comment?: string;
|
||||
expand?: {
|
||||
created_by?: PBUser;
|
||||
approved_by?: PBUser;
|
||||
};
|
||||
}
|
||||
|
||||
interface Args {
|
||||
submissionId: string;
|
||||
dryRun: boolean;
|
||||
ghaOutputPath?: string;
|
||||
}
|
||||
|
||||
interface MetadataAuthor {
|
||||
id: string | number;
|
||||
name?: string;
|
||||
login?: string;
|
||||
}
|
||||
|
||||
interface MetadataEntry {
|
||||
base: string;
|
||||
aliases: string[];
|
||||
categories: string[];
|
||||
update: {
|
||||
timestamp: string;
|
||||
author: MetadataAuthor;
|
||||
};
|
||||
colors?: IconColors;
|
||||
wordmark?: IconWordmark;
|
||||
}
|
||||
|
||||
type VariantKey =
|
||||
| "base"
|
||||
| "light"
|
||||
| "dark"
|
||||
| "wordmark-light"
|
||||
| "wordmark-dark";
|
||||
|
||||
interface VariantTarget {
|
||||
key: VariantKey;
|
||||
destFilename: string;
|
||||
matchers: string[]; // lowercase substrings to prefer when selecting assets
|
||||
sourceAsset?: string; // chosen source asset name
|
||||
}
|
||||
|
||||
const PB_URL = process.env.PB_URL;
|
||||
const PB_ADMIN_TOKEN = process.env.PB_ADMIN_TOKEN;
|
||||
const ICONS_DIR = path.resolve(process.cwd(), "icons");
|
||||
const METADATA_PATH = path.resolve(process.cwd(), "metadata.json");
|
||||
|
||||
function parseArgs(argv: string[]): Args {
|
||||
let submissionId: string | undefined;
|
||||
let dryRun = false;
|
||||
let ghaOutputPath: string | undefined = process.env.GITHUB_OUTPUT;
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg === "--submission-id" && argv[i + 1]) {
|
||||
submissionId = argv[i + 1];
|
||||
i++;
|
||||
} else if (arg === "--dry-run") {
|
||||
dryRun = true;
|
||||
} else if (arg === "--gha-output" && argv[i + 1]) {
|
||||
ghaOutputPath = argv[i + 1];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!submissionId) {
|
||||
throw new Error("Missing required --submission-id");
|
||||
}
|
||||
|
||||
return { submissionId, dryRun, ghaOutputPath };
|
||||
}
|
||||
|
||||
function requireEnv(name: string, value: string | undefined): string {
|
||||
if (!value) {
|
||||
throw new Error(`Missing required env var: ${name}`);
|
||||
}
|
||||
return value.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
async function fetchSubmission(pbUrl: string, id: string): Promise<Submission> {
|
||||
const url = `${pbUrl}/api/collections/submissions/records/${id}?expand=created_by,approved_by`;
|
||||
console.log(`[import-icon] Fetching submission from ${url}`);
|
||||
const res = await fetch(url, {
|
||||
headers: { Authorization: PB_ADMIN_TOKEN ?? "" },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
console.error(
|
||||
`[import-icon] fetch submission failed: status=${res.status} body=${body}`,
|
||||
);
|
||||
throw new Error(`Failed to fetch submission ${id}: ${res.status} ${body}`);
|
||||
}
|
||||
|
||||
return (await res.json()) as Submission;
|
||||
}
|
||||
|
||||
async function ensureDir(dir: string) {
|
||||
await mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
async function fileExists(filePath: string): Promise<boolean> {
|
||||
return await Bun.file(filePath).exists();
|
||||
}
|
||||
|
||||
function inferBase(assets: string[], extrasBase?: string) {
|
||||
if (extrasBase) return extrasBase;
|
||||
const first = assets[0];
|
||||
const ext = path.extname(first).replace(".", "").toLowerCase();
|
||||
return ext || "svg";
|
||||
}
|
||||
|
||||
function buildTargets(submission: Submission): VariantTarget[] {
|
||||
const iconId = submission.name;
|
||||
const ext = inferBase(submission.assets, submission.extras?.base);
|
||||
const assetsLower = submission.assets.map((a) => a.toLowerCase());
|
||||
const hasLight =
|
||||
submission.extras?.colors?.light ||
|
||||
assetsLower.some((a) => a.includes("light"));
|
||||
const hasDark =
|
||||
submission.extras?.colors?.dark ||
|
||||
assetsLower.some((a) => a.includes("dark"));
|
||||
const hasWordmark =
|
||||
submission.extras?.wordmark ||
|
||||
assetsLower.some((a) => a.includes("wordmark"));
|
||||
const hasWordmarkLight =
|
||||
submission.extras?.wordmark?.light ||
|
||||
assetsLower.some((a) => a.includes("wordmark") && a.includes("light"));
|
||||
const hasWordmarkDark =
|
||||
submission.extras?.wordmark?.dark ||
|
||||
assetsLower.some((a) => a.includes("wordmark") && a.includes("dark"));
|
||||
|
||||
const targets: VariantTarget[] = [
|
||||
{ key: "base", destFilename: `${iconId}.${ext}`, matchers: [] },
|
||||
];
|
||||
|
||||
if (hasLight) {
|
||||
targets.push({
|
||||
key: "light",
|
||||
destFilename: `${iconId}-light.${ext}`,
|
||||
matchers: ["light"],
|
||||
});
|
||||
}
|
||||
|
||||
if (hasDark) {
|
||||
targets.push({
|
||||
key: "dark",
|
||||
destFilename: `${iconId}-dark.${ext}`,
|
||||
matchers: ["dark"],
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWordmark || hasWordmarkLight || hasWordmarkDark) {
|
||||
if (hasWordmarkLight || hasWordmark) {
|
||||
targets.push({
|
||||
key: "wordmark-light",
|
||||
destFilename: `${iconId}-wordmark-light.${ext}`,
|
||||
matchers: ["wordmark", "light"],
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWordmarkDark || hasWordmark) {
|
||||
targets.push({
|
||||
key: "wordmark-dark",
|
||||
destFilename: `${iconId}-wordmark-dark.${ext}`,
|
||||
matchers: ["wordmark", "dark"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
function assignAssetsToTargets(
|
||||
assets: string[],
|
||||
targets: VariantTarget[],
|
||||
): VariantTarget[] {
|
||||
const remaining = new Set(assets);
|
||||
|
||||
const takeMatching = (matchers: string[]): string | undefined => {
|
||||
for (const asset of remaining) {
|
||||
const lower = asset.toLowerCase();
|
||||
const allMatch = matchers.every((m) => lower.includes(m));
|
||||
if (allMatch) {
|
||||
remaining.delete(asset);
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const takeAny = (): string | undefined => {
|
||||
const first = remaining.values().next().value as string | undefined;
|
||||
if (first) remaining.delete(first);
|
||||
return first;
|
||||
};
|
||||
|
||||
return targets.map((t) => {
|
||||
const matched = t.matchers.length ? takeMatching(t.matchers) : takeAny();
|
||||
const fallback = matched ?? takeAny();
|
||||
return { ...t, sourceAsset: fallback };
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadAsset(
|
||||
pbUrl: string,
|
||||
submissionId: string,
|
||||
filename: string,
|
||||
destPath: string,
|
||||
) {
|
||||
const url = `${pbUrl}/api/files/submissions/${submissionId}/${encodeURIComponent(filename)}`;
|
||||
const res = await fetch(url, {
|
||||
headers: { Authorization: PB_ADMIN_TOKEN ?? "" },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(
|
||||
`Failed to download asset ${filename}: ${res.status} ${body}`,
|
||||
);
|
||||
}
|
||||
|
||||
const buffer = await res.arrayBuffer();
|
||||
await Bun.write(destPath, buffer);
|
||||
}
|
||||
|
||||
async function readMetadata(): Promise<Record<string, MetadataEntry>> {
|
||||
const file = Bun.file(METADATA_PATH);
|
||||
if (!(await file.exists())) {
|
||||
return {};
|
||||
}
|
||||
const raw = await file.text();
|
||||
return JSON.parse(raw) as Record<string, MetadataEntry>;
|
||||
}
|
||||
|
||||
async function writeMetadata(data: Record<string, MetadataEntry>) {
|
||||
const json = `${JSON.stringify(data, null, 4)}\n`;
|
||||
await Bun.write(METADATA_PATH, json);
|
||||
}
|
||||
|
||||
function buildAuthor(submission: Submission): MetadataAuthor {
|
||||
const creator = submission.expand?.created_by;
|
||||
if (!creator) {
|
||||
return { id: submission.created_by };
|
||||
}
|
||||
|
||||
return {
|
||||
id: creator.id,
|
||||
...(creator.username ? { name: creator.username } : {}),
|
||||
...(creator.email ? { login: creator.email } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildMetadataVariants(assignments: VariantTarget[]) {
|
||||
const colors: IconColors = {};
|
||||
const wordmark: IconWordmark = {};
|
||||
|
||||
for (const v of assignments) {
|
||||
const baseName = v.destFilename.replace(/\.[^.]+$/, "");
|
||||
if (v.key === "light") {
|
||||
colors.light = baseName;
|
||||
} else if (v.key === "dark") {
|
||||
colors.dark = baseName;
|
||||
} else if (v.key === "wordmark-light") {
|
||||
wordmark.light = baseName;
|
||||
} else if (v.key === "wordmark-dark") {
|
||||
wordmark.dark = baseName;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
colors: Object.keys(colors).length ? colors : undefined,
|
||||
wordmark: Object.keys(wordmark).length ? wordmark : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function upsertMetadata(
|
||||
submission: Submission,
|
||||
assignments: VariantTarget[],
|
||||
dryRun: boolean,
|
||||
) {
|
||||
const iconId = submission.name;
|
||||
const base = inferBase(submission.assets, submission.extras?.base);
|
||||
const aliases = submission.extras?.aliases ?? [];
|
||||
const categories = submission.extras?.categories ?? [];
|
||||
const { colors, wordmark } = buildMetadataVariants(assignments);
|
||||
const author = buildAuthor(submission);
|
||||
|
||||
console.log(
|
||||
`[import-icon] Upserting metadata for "${iconId}" base=${base} aliases=${aliases.length} categories=${categories.length}`,
|
||||
);
|
||||
|
||||
const data = await readMetadata();
|
||||
const nextEntry: MetadataEntry = {
|
||||
base,
|
||||
aliases,
|
||||
categories,
|
||||
update: {
|
||||
timestamp: new Date().toISOString(),
|
||||
author,
|
||||
},
|
||||
...(colors ? { colors } : {}),
|
||||
...(wordmark ? { wordmark } : {}),
|
||||
};
|
||||
|
||||
data[iconId] = nextEntry;
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`[dry-run] Would upsert metadata for icon "${iconId}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
await writeMetadata(data);
|
||||
console.log(`Updated metadata for icon "${iconId}"`);
|
||||
}
|
||||
|
||||
async function persistAssets(
|
||||
pbUrl: string,
|
||||
submission: Submission,
|
||||
dryRun: boolean,
|
||||
) {
|
||||
if (submission.assets.length === 0) {
|
||||
throw new Error("Submission has no assets to import");
|
||||
}
|
||||
|
||||
const targets = buildTargets(submission);
|
||||
const assignments = assignAssetsToTargets(submission.assets, targets);
|
||||
|
||||
for (const target of assignments) {
|
||||
if (!target.sourceAsset) {
|
||||
console.warn(
|
||||
`[import-icon] No asset available for variant ${target.key}; skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const destPath = path.join(ICONS_DIR, target.destFilename);
|
||||
console.log(
|
||||
`[import-icon] Handling asset ${target.sourceAsset} -> ${destPath} (variant ${target.key})`,
|
||||
);
|
||||
const exists = await fileExists(destPath);
|
||||
if (exists) {
|
||||
console.log(`Skipping existing asset ${destPath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
`[dry-run] Would download ${target.sourceAsset} -> ${destPath}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await ensureDir(ICONS_DIR);
|
||||
await downloadAsset(pbUrl, submission.id, target.sourceAsset, destPath);
|
||||
console.log(`Downloaded ${target.sourceAsset} -> ${destPath}`);
|
||||
}
|
||||
|
||||
return assignments;
|
||||
}
|
||||
|
||||
async function markSubmissionAdded(
|
||||
pbUrl: string,
|
||||
submissionId: string,
|
||||
dryRun: boolean,
|
||||
) {
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
`[dry-run] Would mark submission ${submissionId} as added_to_collection`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${pbUrl}/api/collections/submissions/records/${submissionId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: PB_ADMIN_TOKEN ?? "",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ status: "added_to_collection" }),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
console.error(
|
||||
`[import-icon] status update failed: status=${res.status} body=${body}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to update submission status: ${res.status} ${body}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Marked submission ${submissionId} as added_to_collection`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const pbUrl = requireEnv("PB_URL", PB_URL);
|
||||
requireEnv("PB_ADMIN_TOKEN", PB_ADMIN_TOKEN);
|
||||
|
||||
console.log(
|
||||
`[import-icon] Starting import submissionId=${args.submissionId} dryRun=${args.dryRun} iconsDir=${ICONS_DIR} metadata=${METADATA_PATH}`,
|
||||
);
|
||||
|
||||
console.log(`Fetching submission ${args.submissionId}...`);
|
||||
const submission = await fetchSubmission(pbUrl, args.submissionId);
|
||||
|
||||
const approver =
|
||||
submission.expand?.approved_by?.username ||
|
||||
submission.expand?.approved_by?.email ||
|
||||
submission.approved_by ||
|
||||
"unknown";
|
||||
|
||||
const assignments = await persistAssets(pbUrl, submission, args.dryRun);
|
||||
await upsertMetadata(submission, assignments, args.dryRun);
|
||||
await markSubmissionAdded(pbUrl, args.submissionId, args.dryRun);
|
||||
|
||||
if (args.ghaOutputPath) {
|
||||
const lines = [
|
||||
`submission_name=${submission.name}`,
|
||||
`approver=${approver}`,
|
||||
].join("\n");
|
||||
await Bun.write(args.ghaOutputPath, new TextEncoder().encode(`${lines}\n`));
|
||||
}
|
||||
|
||||
console.log("Import completed.");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,10 +1,76 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { REPO_PATH } from "@/constants";
|
||||
|
||||
const userSchema = z.object({
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
email: z.string().email(),
|
||||
admin: z.boolean().optional(),
|
||||
avatar: z.string().optional(),
|
||||
created: z.string(),
|
||||
updated: z.string(),
|
||||
});
|
||||
|
||||
const submissionExtrasSchema = z.object({
|
||||
aliases: z.array(z.string()).default([]),
|
||||
categories: z.array(z.string()).default([]),
|
||||
base: z.string().optional(),
|
||||
colors: z
|
||||
.object({
|
||||
dark: z.string().optional(),
|
||||
light: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
wordmark: z
|
||||
.object({
|
||||
dark: z.string().optional(),
|
||||
light: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const submissionSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().min(1, "Icon name is required"),
|
||||
assets: z
|
||||
.array(
|
||||
z.union([
|
||||
z.string(),
|
||||
// Accept File objects while data is still client-side
|
||||
z.instanceof(File),
|
||||
]),
|
||||
)
|
||||
.min(1, "At least one asset is required"),
|
||||
created_by: z.string().optional(),
|
||||
status: z
|
||||
.enum(["approved", "rejected", "pending", "added_to_collection"])
|
||||
.default("pending"),
|
||||
approved_by: z.string().optional(),
|
||||
extras: submissionExtrasSchema.default({
|
||||
aliases: [],
|
||||
categories: [],
|
||||
}),
|
||||
expand: z
|
||||
.object({
|
||||
created_by: userSchema.optional(),
|
||||
approved_by: userSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
created: z.string().optional(),
|
||||
updated: z.string().optional(),
|
||||
admin_comment: z.string().optional().default(""),
|
||||
description: z.string().optional().default(""),
|
||||
});
|
||||
|
||||
export type SubmissionInput = z.infer<typeof submissionSchema>;
|
||||
export const parseSubmission = (input: unknown) =>
|
||||
submissionSchema.parse(input);
|
||||
|
||||
export const ISSUE_TEMPLATES = [
|
||||
{
|
||||
id: "add_monochrome_icon",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"bun-types"
|
||||
],
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
|
||||
Reference in New Issue
Block a user