Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions .github/workflows/pr-metadata.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
name: pr-metadata

# Keep PR triage fields useful without requiring manual cleanup on every branch.
# This workflow only updates PR metadata; it never checks out or executes PR code.
on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]

permissions:
contents: read
issues: write
pull-requests: write

jobs:
triage:
runs-on: ubuntu-latest
env:
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN || secrets.ENGINE_TOKEN }}
TRIAGE_PROJECT_URLS: ${{ vars.TRIAGE_PROJECT_URLS }}
steps:
- name: Assign author and label by changed paths
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const owner = context.repo.owner;
const repo = context.repo.repo;
const issue_number = pr.number;

const assignees = ['TechEngineBot'];
if (!pr.user?.type?.endsWith('Bot')) {
assignees.push(pr.user.login);
}

const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: issue_number,
per_page: 100,
});

const labels = new Set();
const trackers = new Set();
const title = pr.title.toLowerCase();
for (const file of files) {
const path = file.filename;
const isDataDump = path.startsWith('site/public/v1/') || path === 'site/public/openapi.json';
if (path.startsWith('site/') && !isDataDump) {
labels.add('site');
trackers.add('#19');
}
if (path.startsWith('data/') || isDataDump) {
labels.add('data');
trackers.add('#1');
}
if (path.startsWith('app/')) labels.add('app');
if (path.startsWith('.github/workflows/')) labels.add('ci');
if (path.startsWith('docs/') || path === 'README.md' || path.endsWith('.md')) labels.add('documentation');
}
if (title.startsWith('feat') || labels.has('site') || labels.has('data') || labels.has('app')) {
labels.add('enhancement');
}
if (title.startsWith('fix')) {
labels.add('bug');
}

await github.rest.issues.addAssignees({
owner,
repo,
issue_number,
assignees: [...new Set(assignees)],
});

if (labels.size) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number,
labels: [...labels],
});
}

const milestoneByLabel = new Map([
['site', 'Homepage and site improvements'],
['data', 'Massive dataset rebuild (1989-2026)'],
]);
for (const [label, title] of milestoneByLabel) {
if (!labels.has(label)) continue;
const milestones = await github.paginate(github.rest.issues.listMilestones, {
owner,
repo,
state: 'open',
per_page: 100,
});
const milestone = milestones.find((item) => item.title === title);
if (milestone) {
await github.rest.issues.update({
owner,
repo,
issue_number,
milestone: milestone.number,
});
}
break;
}

if (trackers.size) {
const marker = '<!-- techapi-tracking -->';
const existing = pr.body || '';
const refs = [...trackers].sort().map((ref) => `- Refs ${ref}`).join('\n');
const block = `${marker}\n\n## Tracking\n${refs}`;
const body = existing.includes(marker)
? existing.replace(new RegExp(`${marker}[\\s\\S]*$`), block)
: `${existing.trim()}\n\n${block}`.trim();
await github.rest.pulls.update({
owner,
repo,
pull_number: issue_number,
body,
});
}

const changedLines = files.reduce((sum, file) => sum + file.additions + file.deletions, 0);
let priority = 'Medium';
if (labels.has('data') && (files.length >= 25 || changedLines >= 1000)) {
priority = 'High';
} else if (labels.has('data') || labels.has('app') || title.startsWith('fix')) {
priority = 'High';
} else if (labels.size === 1 && labels.has('documentation')) {
priority = 'Low';
} else if (changedLines <= 25 && !labels.has('ci')) {
priority = 'Low';
}
core.exportVariable('TRIAGE_PRIORITY', priority);

- name: Add PR to configured projects
if: env.TRIAGE_PROJECT_URLS != '' && env.PROJECT_TOKEN != ''
env:
GH_TOKEN: ${{ env.PROJECT_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
shell: bash
run: |
set -euo pipefail
IFS=',' read -ra urls <<< "${TRIAGE_PROJECT_URLS}"
today="$(date -u +%F)"
for url in "${urls[@]}"; do
url="$(echo "$url" | xargs)"
[ -z "$url" ] && continue
if [[ "$url" =~ github.com/orgs/([^/]+)/projects/([0-9]+) ]]; then
owner="${BASH_REMATCH[1]}"
project_number="${BASH_REMATCH[2]}"
elif [[ "$url" =~ github.com/users/([^/]+)/projects/([0-9]+) ]]; then
owner="${BASH_REMATCH[1]}"
project_number="${BASH_REMATCH[2]}"
else
echo "::warning::Unsupported project URL: $url"
continue
fi

project_json="$(gh project view "$project_number" --owner "$owner" --format json)"
project_id="$(jq -r '.id' <<< "$project_json")"
item_id="$(gh project item-add "$project_number" --owner "$owner" --url "$PR_URL" --format json --jq '.id')"
start_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Start date") | .id')"
target_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Target date") | .id')"
priority_field_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Priority") | .id')"
priority_option_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq ".fields[] | select(.name == \"Priority\") | .options[] | select(.name == \"${TRIAGE_PRIORITY:-Medium}\") | .id")"
if [ -z "$priority_option_id" ]; then
priority_option_id="$(gh project field-list "$project_number" --owner "$owner" --format json --jq '.fields[] | select(.name == "Priority") | .options[] | select(.name == "Medium") | .id')"
fi

if [ -n "$item_id" ] && [ -n "$start_field_id" ]; then
gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$start_field_id" --date "$today"
fi
if [ -n "$item_id" ] && [ -n "$target_field_id" ]; then
gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$target_field_id" --date "$today"
fi
if [ -n "$item_id" ] && [ -n "$priority_field_id" ] && [ -n "$priority_option_id" ]; then
gh project item-edit --id "$item_id" --project-id "$project_id" --field-id "$priority_field_id" --single-select-option-id "$priority_option_id"
fi
done
13 changes: 9 additions & 4 deletions .github/workflows/request-engine-pr-validation.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
name: request-engine-pr-validation

# Ask TechEngine to validate data PRs and report the result back as a PR
# comment. The ordinary PR check still runs locally; this gives curator-facing
# feedback from the engine repository without moving PR ownership away from the
# human author.
# Ask TechEngine to validate TechAPI PRs and report the result back as PR
# comments. Data changes get engine data checks; homepage changes get the
# TechAPI site build check without moving PR ownership away from the human
# author.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "data/**"
- "site/public/v1/**"
- "site/public/openapi.json"
- "site/src/**"
- "site/public/**"
- "site/astro.config.*"
- "site/package.json"
- "site/package-lock.json"
- "app/validate.py"
workflow_dispatch:
inputs:
Expand Down
33 changes: 31 additions & 2 deletions site/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ const endpoints = [
<span class="mark">T</span>TechAPI<span class="cursor" aria-hidden="true"></span>
</a>
<div class="nav-links">
<a href="#playground">try_it</a>
<a href="#playground">Playground</a>
<a href="#history">History</a>
<a href="#featured">devices</a>
<a href="#endpoints">endpoints</a>
<a href={`${base}docs`}>docs</a>
Expand All @@ -75,7 +76,7 @@ const endpoints = [
</h1>
<p class="tagline">Structured specs &amp; computed scores for smartphones, SoCs, GPUs and CPUs — served as plain static JSON. Free and open.</p>
<div class="hero-cta">
<a class="btn primary" href="#playground">Try the API <span class="arr">→</span></a>
<a class="btn primary" href="#playground">Open Playground <span class="arr">→</span></a>
<a class="btn" href={`${base}docs`}>Read the docs</a>
</div>
</div>
Expand Down Expand Up @@ -104,6 +105,34 @@ const endpoints = [
</header>

<main>
<!-- HISTORY -->
<section class="block" id="history">
<div class="wrap">
<div class="sec-head">
<div>
<span class="kicker">00 - History</span>
<h2 style="margin-top:14px">Dataset growth, synced live</h2>
<p class="sec-sub">Counts come from the published static dump, while recent syncs come from repository activity when GitHub is reachable.</p>
</div>
<span class="num">v1/index.json</span>
</div>

<div class="history" data-reveal>
<div class="history-panel">
<div class="history-label">Current snapshot</div>
<div class="history-total" id="history-total">loading</div>
<div class="history-grid" id="history-counts"></div>
</div>
<div class="history-panel">
<div class="history-label">Recent syncs</div>
<ol class="history-list" id="history-list">
<li><span class="history-dot"></span><span>Loading repository history...</span></li>
</ol>
</div>
</div>
</div>
</section>

<!-- ─────────────── PLAYGROUND ─────────────── -->
<section class="block" id="playground">
<div class="wrap">
Expand Down
55 changes: 54 additions & 1 deletion site/src/scripts/techapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
const raw = import.meta.env.BASE_URL;
const base = raw.endsWith("/") ? raw : raw + "/";
const absUrl = (path) => new URL(path.replace(/^\//, ""), location.origin + base).href;
const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const esc = (s) => String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");

/* ---- theme: follow OS preference until the user picks one (persisted) ---- */
const root = document.documentElement;
Expand Down Expand Up @@ -180,6 +185,54 @@ function countUp(node, target) {
})(performance.now());
}

/* ============================================================
HISTORY
============================================================ */
(function history() {
const totalEl = document.getElementById("history-total");
const countsEl = document.getElementById("history-counts");
const listEl = document.getElementById("history-list");
if (!totalEl || !countsEl || !listEl) return;

const order = ["smartphones", "socs", "gpus", "cpus", "brands"];
const label = { smartphones: "Phones", socs: "SoCs", gpus: "GPUs", cpus: "CPUs", brands: "Brands" };

getJSON("v1/index.json").then((manifest) => {
const rows = order
.map((key) => ({ key, count: manifest.collections?.[key]?.count }))
.filter((row) => row.count != null);
const total = rows.reduce((sum, row) => sum + row.count, 0);
totalEl.textContent = total.toLocaleString() + " records";
countsEl.innerHTML = rows.map((row) =>
`<div class="history-count"><span>${label[row.key]}</span><b>${row.count.toLocaleString()}</b></div>`
).join("");
}).catch(() => {
totalEl.textContent = "sync unavailable";
countsEl.innerHTML = '<div class="history-count"><span>Static dump</span><b>offline</b></div>';
});

fetch("https://api.github.com/repos/GetTechAPI/TechAPI/commits?path=site/public/v1&per_page=5")
.then((response) => {
if (!response.ok) throw new Error(response.statusText);
return response.json();
})
.then((commits) => {
const items = Array.isArray(commits) ? commits.slice(0, 4) : [];
if (!items.length) throw new Error("empty history");
listEl.innerHTML = items.map((item) => {
const message = (item.commit?.message || "Dataset sync").split("\n")[0];
const date = item.commit?.committer?.date ? new Date(item.commit.committer.date) : null;
const when = date ? date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "recent";
const sha = String(item.sha || "").slice(0, 7);
const url = item.html_url || "https://github.com/GetTechAPI/TechAPI";
return `<li><span class="history-dot"></span><span><a href="${esc(url)}" target="_blank" rel="noopener">${esc(message)}</a><small>${esc(when)} · ${esc(sha)}</small></span></li>`;
}).join("");
})
.catch(() => {
listEl.innerHTML = '<li><span class="history-dot"></span><span>Current static dump is available; repository history could not be loaded.<small>GitHub API unavailable</small></span></li>';
});
})();

/* ============================================================
PLAYGROUND
============================================================ */
Expand Down
Loading
Loading