diff --git a/.github/ISSUE_TEMPLATE/project-inventory-ci-alert.md b/.github/ISSUE_TEMPLATE/project-inventory-ci-alert.md new file mode 100644 index 0000000..cd800f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/project-inventory-ci-alert.md @@ -0,0 +1,10 @@ +## Project inventory scheduled check failed + +The scheduled `check-project-inventory` workflow failed. + +- Workflow: `check-project-inventory.yml` +- Trigger: `schedule` +- Repository: `${{ github.repository }}` +- Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + +Please investigate the failure and push a fix. This issue is auto-created once and auto-closed when the scheduled check succeeds again. diff --git a/.github/workflows/check-project-inventory.yml b/.github/workflows/check-project-inventory.yml new file mode 100644 index 0000000..33ec4ed --- /dev/null +++ b/.github/workflows/check-project-inventory.yml @@ -0,0 +1,70 @@ +name: Check project inventory + +on: + pull_request: + paths: + - project-inventory.yml + - project-inventory.schema.yml + - PROJECT_STATUS.md + - scripts/check-project-inventory.js + - package.json + - package-lock.json + - .github/workflows/check-project-inventory.yml + schedule: + - cron: '0 12 * * 1' + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm ci + - run: node scripts/check-project-inventory.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ORG: vega + + - name: Create failure alert issue (scheduled runs) + if: failure() && github.event_name == 'schedule' + uses: peter-evans/create-issue-from-file@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + title: "CI alert: project-inventory scheduled check failing" + labels: ci-alert,project-inventory + content-filepath: .github/ISSUE_TEMPLATE/project-inventory-ci-alert.md + search-existing: open + + - name: Close failure alert issue on recovery (scheduled runs) + if: success() && github.event_name == 'schedule' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const title = "CI alert: project-inventory scheduled check failing"; + const { data: issues } = await github.rest.issues.listForRepo({ + owner, + repo, + state: "open", + labels: "ci-alert,project-inventory", + per_page: 100 + }); + for (const issue of issues) { + if (issue.title === title) { + await github.rest.issues.update({ + owner, + repo, + issue_number: issue.number, + state: "closed" + }); + } + } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md new file mode 100644 index 0000000..9b8bf4b --- /dev/null +++ b/PROJECT_STATUS.md @@ -0,0 +1,51 @@ +# Project Status Definitions + +This document defines public metadata values used by [`project-inventory.yml`](./project-inventory.yml). + +Field usage: + +- `support_level` is optional; default inference is `active -> full`, `experimental -> best_effort`, `maintenance -> best_effort`, `deprecated -> none`, `archived -> none`. +- `replacement` is optional; include when a clear migration target exists. +- `notes` is optional; use sparingly for exceptions not captured by enum fields. +- `uncategorized_repos` holds known public repos not yet fully classified (often lightly maintained, historical, or low-signal). +- `core_maintainers` is optional; use when a repository has clear, consistent maintainer leadership. +- `research_prototypes` groups research repositories sharing common lifecycle/support/ownership defaults. +- `archived_repos` is a flat list of historical repositories; entries map to `lifecycle_status: archived`, `project_role: historical`, `owner_model: unowned`, and `support_level: none`. + +## `lifecycle_status` + +| Value | Meaning | +| --- | --- | +| `active` | Normal development. Issues, pull requests, and releases are expected. | +| `experimental` | Exploratory work. APIs, scope, or long-term maintenance may change. | +| `maintenance` | Kept working, but limited new feature development. | +| `deprecated` | Users should migrate elsewhere; list a replacement where possible. | +| `archived` | Historical or read-only. No support expected. | + +## `support_level` + +| Value | Meaning | +| --- | --- | +| `full` | Maintainers review issues/security reports and publish releases as appropriate. | +| `best_effort` | Maintained when time allows; no strong response commitment. | +| `none` | Unsupported; no maintainer response expected. | + +## `project_role` + +| Value | Meaning | +| --- | --- | +| `primary` | Main Vega/Altair library, runtime, grammar, service, schema, editor, or public-facing site. | +| `ecosystem` | Important package, helper, extension, dataset, or ecosystem utility. | +| `integration` | Framework, platform, package-manager, documentation, or external-system integration. | +| `example` | Tutorial, demo, sample, benchmark, or teaching material. | +| `research` | Research prototype, exploratory system, proposal, or academic project. | +| `infrastructure` | Build, deployment, governance, CI, org metadata, or operational tooling. | +| `historical` | Superseded, archived, version-specific, or retained for historical reference. | + +## `owner_model` + +| Value | Meaning | +| --- | --- | +| `org_shared` | The Vega maintainer group collectively owns the repository. | +| `named_lead` | We have a primary maintainer for triage, release, or review leadership | +| `unowned` | No active maintainer; reserved for archived or unsupported repositories. | diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dd06839 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,288 @@ +{ + "name": "vega-org-metadata", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vega-org-metadata", + "version": "1.0.0", + "dependencies": { + "@octokit/rest": "^22.0.1", + "ajv": "^8.17.1", + "yaml": "^2.8.1" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.9", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.9.tgz", + "integrity": "sha512-o8Bi3f608eyM+7BmBiUWxFsdjLb3/ym1cQek5LZOv9KkZcxRrHCPhhRzm6xjO6HVZ85ItD6+sTsjxo821SVa/A==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "content-type": "^2.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" + }, + "node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-with-bigint": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d0e7f0a --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "vega-org-metadata", + "private": true, + "version": "1.0.0", + "description": "Validation tooling for Vega GitHub org project inventory", + "scripts": { + "check:project-inventory": "node scripts/check-project-inventory.js" + }, + "dependencies": { + "@octokit/rest": "^22.0.1", + "ajv": "^8.17.1", + "yaml": "^2.8.1" + } +} diff --git a/project-inventory.schema.yml b/project-inventory.schema.yml new file mode 100644 index 0000000..eece12c --- /dev/null +++ b/project-inventory.schema.yml @@ -0,0 +1,156 @@ +$schema: https://json-schema.org/draft/2020-12/schema +type: object +required: + - metadata_version + - repos +properties: + metadata_version: + type: integer + definitions: + type: string + schema: + type: string + # Known public repositories that are intentionally not fully classified yet. + uncategorized_repos: + type: array + items: + type: string + # Primary categorized repositories. support_level is optional and may be inferred + # from lifecycle_status by validator defaults. + repos: + type: array + items: + type: object + additionalProperties: false + required: + - name + - lifecycle_status + - project_role + - owner_model + properties: + name: + type: string + lifecycle_status: + enum: + - active + - experimental + - maintenance + - deprecated + - archived + support_level: + enum: + - full + - best_effort + - none + project_role: + enum: + - primary + - ecosystem + - integration + - example + - research + - infrastructure + - historical + owner_model: + enum: + - org_shared + - named_lead + - unowned + replacement: + type: string + notes: + type: string + repo_lead: + type: + - string + - 'null' + backup: + type: + - string + - 'null' + package_names: + type: array + items: + type: string + core_maintainers: + type: array + items: + type: string + # Research repos grouped with shared defaults to avoid repetitive metadata. + research_prototypes: + type: object + required: + - defaults + - repos + additionalProperties: false + properties: + defaults: + type: object + additionalProperties: false + required: + - lifecycle_status + - project_role + - owner_model + properties: + lifecycle_status: + enum: + - active + - experimental + - maintenance + - deprecated + - archived + support_level: + enum: + - full + - best_effort + - none + project_role: + enum: + - research + owner_model: + enum: + - org_shared + - named_lead + - unowned + repos: + type: array + items: + oneOf: + - type: string + - type: object + additionalProperties: false + required: + - name + properties: + name: + type: string + lifecycle_status: + enum: + - active + - experimental + - maintenance + - deprecated + - archived + support_level: + enum: + - full + - best_effort + - none + project_role: + enum: + - research + owner_model: + enum: + - org_shared + - named_lead + - unowned + core_maintainers: + type: array + items: + type: string + # Archived repos as a flat list. Archived implies lifecycle_status:archived, + # project_role:historical, owner_model:unowned, support_level:none. + archived_repos: + type: array + items: + type: string diff --git a/project-inventory.yml b/project-inventory.yml new file mode 100644 index 0000000..80c764c --- /dev/null +++ b/project-inventory.yml @@ -0,0 +1,219 @@ +metadata_version: 1 +definitions: ./PROJECT_STATUS.md +schema: ./project-inventory.schema.yml +repos: + - name: .github + lifecycle_status: active + project_role: infrastructure + owner_model: org_shared + - name: altair + lifecycle_status: active + project_role: primary + owner_model: org_shared + core_maintainers: + - joelostblom + - mattijn + - name: dash-vega-components + lifecycle_status: maintenance + project_role: integration + owner_model: org_shared + - name: editor + lifecycle_status: active + project_role: primary + owner_model: named_lead + core_maintainers: + - domoritz + repo_lead: domoritz + - name: editor-backend + lifecycle_status: maintenance + project_role: primary + owner_model: org_shared + - name: react-vega + lifecycle_status: maintenance + project_role: integration + owner_model: org_shared + - name: roadmap + lifecycle_status: maintenance + project_role: infrastructure + owner_model: org_shared + - name: schema + lifecycle_status: active + project_role: primary + owner_model: org_shared + - name: sphinxext-altair + lifecycle_status: maintenance + project_role: integration + owner_model: org_shared + - name: svelte-vega + lifecycle_status: maintenance + project_role: integration + owner_model: org_shared + - name: ts-json-schema-generator + lifecycle_status: active + support_level: best_effort + project_role: infrastructure + owner_model: org_shared + - name: vega + lifecycle_status: active + project_role: primary + owner_model: named_lead + core_maintainers: + - hydrosquall + repo_lead: hydrosquall + - name: vega-datasets + lifecycle_status: maintenance + project_role: ecosystem + owner_model: named_lead + core_maintainers: + - dsmedia + repo_lead: dsmedia + - name: vega-embed + lifecycle_status: active + project_role: primary + owner_model: org_shared + - name: vega-lite + lifecycle_status: active + project_role: primary + owner_model: named_lead + repo_lead: domoritz + core_maintainers: + - domoritz + - name: vega-lite-api + lifecycle_status: maintenance + project_role: integration + owner_model: org_shared + - name: vega-loader-arrow + lifecycle_status: maintenance + project_role: integration + owner_model: org_shared + - name: vega-loader-parquet + lifecycle_status: maintenance + project_role: integration + owner_model: org_shared + - name: vega-themes + lifecycle_status: maintenance + project_role: ecosystem + owner_model: org_shared + - name: vega-tooltip + lifecycle_status: maintenance + project_role: ecosystem + owner_model: org_shared + - name: vega.github.io + lifecycle_status: active + project_role: primary + owner_model: org_shared + - name: vegafusion + lifecycle_status: active + project_role: ecosystem + owner_model: named_lead + core_maintainers: + - jonmmease + repo_lead: jonmmease + - name: vl-convert + lifecycle_status: maintenance + project_role: ecosystem + owner_model: org_shared + - name: vl-convert-service + lifecycle_status: maintenance + project_role: integration + owner_model: org_shared + - name: vue-vega + lifecycle_status: maintenance + project_role: integration + owner_model: org_shared +uncategorized_repos: + - altair_ally + - altair_tiles + - compassql-analysis + - dataflow-api + - dataflow-vis + - datalib + - datalib-sketch + - ipyvega + - logos + - polestar + - scalable-vega + - SciPy2024-Altair-Tutorial + - ts-api + - vega-benchmarks + - vega-bundler + - vega-dataflow-examples + - vega-gist + - vega-lite-params-proposal + - vega-lite-pr-previews + - vega-lite-shorthand + - vega-lite-to-api + - vega-lite-transforms2sql + - vega-lite-tutorials + - vega-logging + - vega-plus + - vega-renderer-webgl + - vega-tile + - vega-tutorials + - vega-webgl-renderer + - vega-webgpu + - vega2-docs + - vegafusion-conda-feedstock + - vegafusion-conda-jupyter-feedstock + - vegafusion-conda-python-embed-feedstock + - vegafusion-demos + - vegafusion-logos + - vg-transforms2sql + - vl-convert-python-feedstock + - web-service +research_prototypes: + defaults: + lifecycle_status: maintenance + project_role: research + owner_model: org_shared + repos: + - compassql + - falcon + - lyra +archived_repos: + - voyager + - voyager-docs + - voyager-electron + - voyager-server + - voyager2 + - compass + - react-vega-lite + - vega-canvas + - vega-crossfilter + - vega-dataflow + - vega-desktop + - vega-editor + - vega-embed-v2 + - vega-encode + - vega-event-selector + - vega-expression + - vega-force + - vega-geo + - vega-hierarchy + - vega-label + - vega-lib + - vega-lite-dev-config + - vega-lite-ui + - vega-lite-v1 + - vega-lite-v2 + - vega-lite-v3 + - vega-lite-v4 + - vega-loader + - vega-observable + - vega-parser + - vega-projection + - vega-projection-extended + - vega-render-service + - vega-runtime + - vega-scale + - vega-scenegraph + - vega-statistics + - vega-transforms + - vega-typings + - vega-util + - vega-view + - vega-view-transforms + - vega-voronoi + - vega-vscode + - vega-wordcloud + - web-service diff --git a/scripts/check-project-inventory.js b/scripts/check-project-inventory.js new file mode 100644 index 0000000..a00a0e4 --- /dev/null +++ b/scripts/check-project-inventory.js @@ -0,0 +1,173 @@ +#!/usr/bin/env node + +const fs = require('node:fs/promises'); +const path = require('node:path'); +const process = require('node:process'); +const YAML = require('yaml'); +const Ajv2020 = require('ajv/dist/2020'); +const { Octokit } = require('@octokit/rest'); + +const ROOT = process.cwd(); +const INVENTORY_PATH = path.join(ROOT, 'project-inventory.yml'); +const SCHEMA_PATH = path.join(ROOT, 'project-inventory.schema.yml'); +const SUPPORT_DEFAULTS = { + active: 'full', + experimental: 'best_effort', + maintenance: 'best_effort', + deprecated: 'none', + archived: 'none', +}; + +function formatAjvErrors(errors = []) { + return errors.map((err) => { + const loc = err.instancePath || '/'; + return `- ${loc} ${err.message}`; + }); +} + +async function readYaml(filePath) { + const raw = await fs.readFile(filePath, 'utf8'); + return YAML.parse(raw); +} + +function warn(msg) { + console.warn(`[warn] ${msg}`); +} + +function fail(msg) { + console.error(`[error] ${msg}`); +} + +function supportLevelFor(entry) { + return entry.support_level || SUPPORT_DEFAULTS[entry.lifecycle_status]; +} + +async function fetchPublicRepos(org, token) { + const octokit = new Octokit({ auth: token }); + const repos = await octokit.paginate(octokit.repos.listForOrg, { + org, + type: 'public', + per_page: 100, + }); + return repos; +} + +async function main() { + const inventory = await readYaml(INVENTORY_PATH); + const schema = await readYaml(SCHEMA_PATH); + + const ajv = new Ajv2020({ allErrors: true, strict: false }); + const validate = ajv.compile(schema); + const valid = validate(inventory); + + if (!valid) { + fail('Inventory failed schema validation.'); + for (const err of formatAjvErrors(validate.errors)) { + fail(err); + } + process.exit(1); + } + + const repoEntries = inventory.repos || []; + const researchDefaults = inventory.research_prototypes?.defaults || null; + const researchEntriesRaw = inventory.research_prototypes?.repos || []; + const researchEntries = researchEntriesRaw.map((entry) => { + if (typeof entry === 'string') { + return { ...(researchDefaults || {}), name: entry }; + } + return { ...(researchDefaults || {}), ...entry }; + }); + const archivedEntriesRaw = inventory.archived_repos || []; + const archivedEntries = archivedEntriesRaw.map((name) => ({ + name, + lifecycle_status: 'archived', + project_role: 'historical', + owner_model: 'unowned', + support_level: 'none', + })); + const allEntries = [...repoEntries, ...researchEntries, ...archivedEntries]; + const uncategorized = new Set(inventory.uncategorized_repos || []); + const inventoryByName = new Map(); + let hasErrors = false; + + for (const entry of allEntries) { + if (inventoryByName.has(entry.name)) { + fail(`Duplicate inventory entry for repository '${entry.name}'.`); + hasErrors = true; + continue; + } + inventoryByName.set(entry.name, entry); + + const effectiveSupportLevel = supportLevelFor(entry); + + if (entry.lifecycle_status === 'archived' && effectiveSupportLevel !== 'none') { + fail(`'${entry.name}' is archived but support_level is not 'none'.`); + hasErrors = true; + } + + if (entry.owner_model === 'named_lead' && !entry.repo_lead) { + fail(`'${entry.name}' uses owner_model 'named_lead' but repo_lead is missing.`); + hasErrors = true; + } + + if (entry.owner_model === 'unowned') { + const hasSupportNone = effectiveSupportLevel === 'none'; + const hasClarifyingNote = typeof entry.notes === 'string' && entry.notes.trim().length > 0; + if (!hasSupportNone && !hasClarifyingNote) { + warn(`'${entry.name}' is 'unowned' but neither support_level:none nor clarifying notes were provided.`); + } + } + + if (entry.lifecycle_status === 'deprecated' && !entry.replacement) { + warn(`'${entry.name}' is ${entry.lifecycle_status} and has no replacement set. Verify this is intentional.`); + } + } + + const token = process.env.GITHUB_TOKEN; + const org = process.env.GITHUB_ORG || 'vega'; + + if (!token) { + warn('GITHUB_TOKEN is not set; skipping GitHub org drift checks.'); + process.exit(hasErrors ? 1 : 0); + } + + const orgRepos = await fetchPublicRepos(org, token); + const orgRepoNames = new Set(orgRepos.map((repo) => repo.name)); + + for (const repoName of orgRepoNames) { + if (!inventoryByName.has(repoName) && !uncategorized.has(repoName)) { + fail(`Public repository '${repoName}' is missing from project-inventory.yml.`); + hasErrors = true; + } + } + + for (const [repoName, entry] of inventoryByName.entries()) { + if (!orgRepoNames.has(repoName)) { + fail(`Inventory entry '${repoName}' does not match an existing public repository in ${org}.`); + hasErrors = true; + continue; + } + + const repo = orgRepos.find((r) => r.name === repoName); + if (repo && repo.archived && entry.lifecycle_status !== 'archived') { + const hasExplicitReason = typeof entry.notes === 'string' && entry.notes.trim().length > 0; + if (!hasExplicitReason) { + fail(`'${repoName}' is archived on GitHub but lifecycle_status is '${entry.lifecycle_status}' with no explicit note.`); + hasErrors = true; + } else { + warn(`'${repoName}' is archived on GitHub but inventory lifecycle_status is '${entry.lifecycle_status}'. Notes provided; confirm intent.`); + } + } + } + + if (hasErrors) { + process.exit(1); + } + + console.log('Project inventory checks passed.'); +} + +main().catch((err) => { + fail(err.stack || String(err)); + process.exit(1); +});