Dataset growth, synced live
+Counts come from the published static dump, while recent syncs come from repository activity when GitHub is reachable.
+-
+
- Loading repository history... +
diff --git a/.github/workflows/pr-metadata.yml b/.github/workflows/pr-metadata.yml new file mode 100644 index 00000000000..0a5e894ccab --- /dev/null +++ b/.github/workflows/pr-metadata.yml @@ -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 = ''; + 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 diff --git a/.github/workflows/request-engine-pr-validation.yml b/.github/workflows/request-engine-pr-validation.yml index de3d8ce962a..0bec07247b5 100644 --- a/.github/workflows/request-engine-pr-validation.yml +++ b/.github/workflows/request-engine-pr-validation.yml @@ -1,9 +1,9 @@ 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] @@ -11,6 +11,11 @@ on: - "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: diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index 7895800f1da..9af43135f92 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -48,7 +48,8 @@ const endpoints = [ TTechAPI
@@ -104,6 +105,34 @@ const endpoints = [Counts come from the published static dump, while recent syncs come from repository activity when GitHub is reachable.
+