diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3f52241..9c2f50f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,5 +4,5 @@ contact_links: url: https://acecore.net/contact/ about: 事業相談や非公開の連絡は公式お問い合わせフォームを利用してください。 - name: セキュリティ報告 - url: https://acecore.net/contact/ + url: https://github.com/acecore-systems/.github/blob/main/SECURITY.md about: 脆弱性は公開 Issue に投稿せず、まずセキュリティポリシーと連絡窓口を確認してください。 diff --git a/.github/workflows/community-check.yml b/.github/workflows/community-check.yml new file mode 100644 index 0000000..0d7414a --- /dev/null +++ b/.github/workflows/community-check.yml @@ -0,0 +1,29 @@ +name: Community settings + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: community-check-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + community-check: + name: community-check + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + cache: npm + - run: npm ci + - run: npm run check 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/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..ceef3a3 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "proseWrap": "preserve", + "endOfLine": "lf" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf2556d..e503ce7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,13 +14,14 @@ Acecore プロジェクトへの改善提案ありがとうございます。 ## Issue -可能な限り Issue テンプレートを使ってください。 +共通の Issue テンプレートは次の2種類です。独自テンプレートがあるリポジトリでは、そちらの案内に従ってください。 -- バグ報告には、期待される動作、実際の動作、再現手順、関連する環境情報を - 含めてください。 -- 機能要望には、解決したい課題、望ましい結果、制約条件を含めてください。 -- コンテンツやドキュメントの変更では、対象ページ、言語、ファイルパスを - 明記してください。 +- **不具合**: 現在の挙動、期待する挙動、直ったと判断できる状態を記載してください。 + 再現手順や環境情報は、分かる範囲で添えてください。 +- **タスク**: 改善、機能追加、コンテンツ、CMS、運用などの依頼に使います。 + 何をしたいかと完了条件を記載し、対象ページ、言語、ファイルパスや制約があれば添えてください。 + +迷った場合は「タスク」を使い、足りない情報はコメントで補います。空の Issue も利用できます。 ## Pull Request diff --git a/README.md b/README.md index cd7a483..32ea104 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,65 @@ - `.github/ISSUE_TEMPLATE/`: 共通の Issue フォーム - `.github/PULL_REQUEST_TEMPLATE.md`: 共通の Pull Request テンプレート -各リポジトリに同名のファイルがある場合は、リポジトリ側の設定が優先されます。 +## 共通設定が使われる条件 + +この公開リポジトリのデフォルトブランチにある共通ファイルは、`acecore-systems` が +所有するリポジトリで、対応する個別設定がない場合にGitHub上で使われます。 +継承先の公開・非公開は問いません。共通ファイルは継承先にコピーされず、cloneにも含まれません。 + +| 対象 | 個別設定との関係 | +| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| CONTRIBUTING・CODE_OF_CONDUCT・SECURITY・SUPPORT | 各リポジトリにその種類のファイルがあれば個別版を優先します。複数の配置場所が使えるファイルは `.github/` → ルート → `docs/` の順で探します。 | +| Issue Forms・問い合わせ設定 | 個別リポジトリの `.github/ISSUE_TEMPLATE/` に有効なテンプレートまたは `config.yml` があれば、共通フォルダーの内容は一切使われません。フォーム単位では合成されません。 | +| PRテンプレート | 個別テンプレートがある場合は個別版を使います。 | +| `profile/README.md` | Organizationの公開プロフィールに表示されます。個別repoのREADMEにはなりません。 | + +GitHub Actions、`AGENTS.md`、`.github/copilot-instructions.md` は、ここに置くだけでは +他のリポジトリへ自動適用されません。Actionsは各repoにworkflowを配置するか、 +呼び出し側を用意して再利用します。エージェント向け指示も必要な各repoで管理してください。 + +共通設定を使う場合は、個別設定の必要性と差分を確認したうえで、その種類の個別設定を取り除きます。 +独自設定が必要な場合は、そのrepoで管理します。特に独自Issue設定では、必要な非公開連絡先も +個別の `config.yml` に記載してください。 + +詳しくは [GitHubの共通ファイル仕様](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/creating-a-default-community-health-file) を参照してください。 + +## 検証 + +Node.js 24とnpmを使います。依存関係はlockfileに固定しています。 + +```sh +npm ci +npm run check +``` + +`npm run check` は以下を実行します。 + +- PrettierによるMarkdown・YAML・検証コードの整形確認。 +- YAML構文、Issue Formsの基本構造(必須キー・入力型・IDやラベルの重複など)、問い合わせ設定の検証。 +- Markdownの通常リンク・画像・参照リンクの相対パス確認。 +- 検証処理が誤りを検出できることを確認するテスト。 + +自動修正は `npm run format`、構造とリンクだけの確認は `npm run check:community` です。 +構造検証はGitHubの全仕様を再実装するものではありません。外部URLの応答、見出しアンカー、 +HTMLで記述したリンク、GitHub上のフォーム表示は自動チェックの対象外です。 +相対パスはこのrepo内に存在するファイルやディレクトリを指す必要があります。 + +CIはPRと `main` へのpushで `community-check` を実行します。必須チェックとして使うため、 +パスによる実行除外や `[skip ci]` は使わないでください。外部サービスの認証情報は不要です。 + +## 更新手順と影響確認 + +1. 最新の `origin/main` から専用branch/worktreeを作成し、他の未コミット作業を混ぜずに変更します。 +2. Issueは「不具合・タスク」の2種類を基本にし、必須入力やPR本文をむやみに増やしません。 +3. `npm run check` と `git diff --check` を実行し、変更した外部リンクの到達先も確認します。 +4. 日本語のdraft PRに変更理由・検証結果・適用先への影響を記載します。CI成功後にレビューを受けます。 +5. マージ後、Organizationの公開プロフィール、文書リンク、Issue選択画面とフォーム表示を確認します。 + +継承確認には、共通Issue設定を使う `homepage-hatt` と、独自設定を持つ `acecore-net` を使います。 +確認時に各repoのデフォルトブランチの設定を読み、前者には共通変更が反映され、後者の独自フォームは +維持されていることを確認してください。テンプレートの確認だけでIssueを投稿する必要はありません。 + +初回導入ではPRの `community-check` 成功後に、既存の `main` 保護へ同名チェックを必須として追加します。 +レビュー人数や管理者への適用など、他の保護項目は維持します。 +チェックを改名する場合は、旧名の必須設定がマージを止めないよう、workflowと保護設定を一緒に見直します。 diff --git a/SUPPORT.md b/SUPPORT.md index d97abe3..8f5910b 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -3,12 +3,13 @@ プロジェクト固有の相談や作業依頼は、各リポジトリの Issue テンプレートを 利用してください。 -- バグ: 再現手順と期待される動作を含めて報告してください。 -- 機能要望: 解決したい課題と望ましい結果を説明してください。 -- ドキュメント・コンテンツ変更: 対象のパス、言語、ページを明記してください。 +- **不具合**: 現在の挙動と期待する挙動、直ったと判断できる状態を報告してください。再現手順は分かる範囲で構いません。 +- **タスク**: 改善、機能追加、ドキュメント・コンテンツ、CMS、運用などの依頼に使います。依頼内容と完了条件、分かれば対象のパス・言語・ページを記載してください。 + +迷った場合は「タスク」を使ってください。独自テンプレートがあるリポジトリでは、その案内に従ってください。 事業に関する問い合わせやWebサイト経由の連絡は、以下を利用してください。 https://acecore.net/contact/ -セキュリティ報告は `SECURITY.md` に従い、公開の場には投稿しないでください。 +セキュリティ報告は [セキュリティポリシー](https://github.com/acecore-systems/.github/blob/main/SECURITY.md) に従い、公開の場には投稿しないでください。リポジトリ固有の手順がある場合は、そちらを優先してください。 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1ada9b0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,784 @@ +{ + "name": "acecore-community-settings", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "acecore-community-settings", + "devDependencies": { + "prettier": "3.9.6", + "remark-parse": "11.0.0", + "unified": "11.0.5", + "yaml": "2.9.1" + }, + "engines": { + "node": "24.x" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "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..f4b0734 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "acecore-community-settings", + "private": true, + "type": "module", + "engines": { + "node": "24.x" + }, + "scripts": { + "check": "npm run format:check && npm run check:community && npm test", + "format:check": "prettier --check .", + "format": "prettier --write .", + "check:community": "node scripts/check-community.mjs", + "test": "node --test" + }, + "devDependencies": { + "prettier": "3.9.6", + "remark-parse": "11.0.0", + "unified": "11.0.5", + "yaml": "2.9.1" + } +} diff --git a/profile/README.md b/profile/README.md index bc7415a..3de5547 100644 --- a/profile/README.md +++ b/profile/README.md @@ -1,50 +1,24 @@ # Acecore -システム開発・Web制作・サーバー運用・デザイン・IT教育を展開する -**Acecore** の GitHub Organization です。 +Web・業務システム開発、IT教育、コミュニティ運営に取り組む株式会社AcecoreのGitHub Organizationです。 +公開プロジェクトのコードや設計文書を通じて、開発と運用の取り組みを紹介しています。 -公式サイトや技術ブログ、教育事業、コミュニティ運営、公開設計ドキュメントを -通じて、Webと学びのための実用的な仕組みを整えています。 +[公式サイト](https://acecore.net/) · [お問い合わせ](https://acecore.net/contact/) -## 事業内容 +## 公開プロジェクト -- **業務システム・アプリ開発** — 業務効率化、データ管理、社内ツール、Webアプリケーションの企画・開発・運用 -- **Webサイト制作・運用** — コーポレートサイト、サービスサイト、ブログ、問い合わせ導線まで含めた制作・改善 -- **サーバー構築・運用** — 公開サーバーやWebサービスを支えるインフラ設計、運用、改善 -- **デザイン・クリエイティブ** — ロゴ、名刺、バナー、キャラクターなど、伝えたい価値を形にする制作 -- **IT教育 / Acecore Schools** — 子どもから大人までを対象にしたプログラミング・IT学習支援 -- **コミュニティ運営** — [Aceserver](https://asv.acecore.net/) など、技術と人がつながる場づくり +| プロジェクト | 内容 | 利用・閲覧 | +| ----------------------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | +| [acecore-net](https://github.com/acecore-systems/acecore-net) | コーポレートサイト。サービスや技術ブログを公開しています。 | [公式サイト](https://acecore.net/) | +| [acecore-schools](https://github.com/acecore-systems/acecore-schools) | Acecore Schoolsの公式サイトと予約・会員ポータル。 | [Schools](https://schools.acecore.net/) | +| [cms-ai](https://github.com/acecore-systems/cms-ai) | Sveltia CMS向けのAI会話・権限・PR実装を支える共通基盤。 | [README](https://github.com/acecore-systems/cms-ai#readme) | +| [world-foundation](https://github.com/acecore-systems/world-foundation) | 自由参加・離脱・フォークができる協力基盤の公開設計文書。 | [閲覧サイト](https://world-foundation-site.pages.dev/) | -## 主要公開リポジトリ +## 利用・参加する -### [acecore-net](https://github.com/acecore-systems/acecore-net) +- **利用する**: 上記のサイトやREADMEをご覧ください。Minecraftコミュニティは [Aceserver](https://asv.acecore.net/) で案内しています。 +- **不具合を報告する**: 対象プロジェクトのIssuesから報告してください。共通フォームでは「不具合」を使います。 +- **改善に参加する**: 対象プロジェクトのREADMEと貢献手順を確認し、IssueやPRで提案してください。共通フォームでは「タスク」を使います。共通の手順は [コントリビューションガイド](https://github.com/acecore-systems/.github/blob/main/CONTRIBUTING.md) にまとめています。 -Acecore のコーポレートサイトを管理するリポジトリです。Astro、UnoCSS、 -Cloudflare Pages を使い、サービス情報、スクール情報、ブログ、問い合わせ導線を -静的サイトとして運用しています。 - -- [公式サイト](https://acecore.net) -- [サービス](https://acecore.net/services/) -- [ブログ](https://acecore.net/blog/) -- [プログラミングスクール](https://acecore.net/schools/) -- [AceStudio](https://acecore.net/acestudio/) -- [お問い合わせ](https://acecore.net/contact/) - -### [world-foundation](https://github.com/acecore-systems/world-foundation) - -自由参加・離脱可能・フォーク可能な協力基盤を考える公開設計ドキュメントです。 -社会制度や協力の仕組みを、Issue、Pull Request、Proposal、Decision、Glossary、 -Module へ分解し、ソフトウェア設計のようにレビューできる形で整理しています。 - -- [基本設計](https://github.com/acecore-systems/world-foundation/blob/main/docs/ja/README.md) -- [図表](https://github.com/acecore-systems/world-foundation/tree/main/assets/diagrams) -- [提案](https://github.com/acecore-systems/world-foundation/tree/main/proposals) -- [意思決定](https://github.com/acecore-systems/world-foundation/tree/main/decisions) -- [用語集](https://github.com/acecore-systems/world-foundation/tree/main/glossary) - -## リンク - -- 🌐 [acecore.net](https://acecore.net) -- 📝 [Acecore Blog](https://acecore.net/blog/) -- 🎮 [Aceserver](https://asv.acecore.net/) -- 📬 [お問い合わせ](https://acecore.net/contact/) +事業のご相談は [お問い合わせ](https://acecore.net/contact/) へ。 +脆弱性の報告は公開Issueに投稿せず、対象repoの手順、または [共通セキュリティポリシー](https://github.com/acecore-systems/.github/blob/main/SECURITY.md) を確認してください。 diff --git a/scripts/check-community.mjs b/scripts/check-community.mjs new file mode 100644 index 0000000..9b22c5d --- /dev/null +++ b/scripts/check-community.mjs @@ -0,0 +1,236 @@ +import { readdir, readFile, stat } from "node:fs/promises"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { parseDocument } from "yaml"; +import { unified } from "unified"; +import remarkParse from "remark-parse"; + +const markdown = unified().use(remarkParse); +const inputTypes = new Set([ + "checkboxes", + "dropdown", + "input", + "markdown", + "textarea", + "upload", +]); +const isObject = (value) => + value !== null && typeof value === "object" && !Array.isArray(value); +const isText = (value) => typeof value === "string" && value.trim().length > 0; + +// This checks the form structure used by this repository, not every GitHub option. +export function validateForm(form) { + const errors = []; + if (!isObject(form)) return ["form must be a mapping"]; + for (const key of ["name", "description"]) { + if (!isText(form[key])) errors.push(`${key} must be a non-empty string`); + } + if (!Array.isArray(form.body) || form.body.length === 0) { + return [...errors, "body must be a non-empty array"]; + } + const ids = new Set(); + const labels = new Set(); + let inputs = 0; + form.body.forEach((field, index) => { + const location = `body[${index}]`; + if (!isObject(field) || !inputTypes.has(field.type)) { + errors.push(`${location}.type must be a supported GitHub input type`); + return; + } + if (field.id !== undefined) { + if (typeof field.id !== "string" || !/^[A-Za-z0-9_-]+$/.test(field.id)) { + errors.push( + `${location}.id must contain only letters, numbers, _ or -`, + ); + } else if (ids.has(field.id)) { + errors.push(`${location}.id duplicates ${field.id}`); + } + ids.add(field.id); + } + if (!isObject(field.attributes)) { + errors.push(`${location}.attributes must be a mapping`); + return; + } + const attributes = field.attributes; + if (field.type === "markdown") { + if (!isText(attributes.value)) + errors.push(`${location}.attributes.value must be text`); + } else { + inputs += 1; + if (!isText(attributes.label)) { + errors.push(`${location}.attributes.label must be text`); + } else if (labels.has(attributes.label)) { + errors.push( + `${location}.attributes.label duplicates ${attributes.label}`, + ); + } + labels.add(attributes.label); + } + if ( + field.validations !== undefined && + (!isObject(field.validations) || + (field.validations.required !== undefined && + typeof field.validations.required !== "boolean")) + ) { + errors.push(`${location}.validations.required must be a boolean`); + } + if (["dropdown", "checkboxes"].includes(field.type)) { + if ( + !Array.isArray(attributes.options) || + attributes.options.length === 0 + ) { + errors.push(`${location}.attributes.options must be a non-empty array`); + } else { + for (const option of attributes.options) { + const valid = + field.type === "dropdown" + ? isText(option) + : isObject(option) && + isText(option.label) && + (option.required === undefined || + typeof option.required === "boolean"); + if (!valid) + errors.push( + `${location}.attributes.options contains an invalid option`, + ); + } + } + } + }); + if (inputs === 0) errors.push("body must include at least one input field"); + return errors; +} + +export function validateConfig(config) { + if (!isObject(config)) return ["config must be a mapping"]; + const errors = []; + if ( + config.blank_issues_enabled !== undefined && + typeof config.blank_issues_enabled !== "boolean" + ) { + errors.push("blank_issues_enabled must be a boolean"); + } + if (config.contact_links !== undefined) { + if (!Array.isArray(config.contact_links)) + return [...errors, "contact_links must be an array"]; + config.contact_links.forEach((link, index) => { + if (!isObject(link) || !isText(link.name) || !isText(link.about)) { + errors.push(`contact_links[${index}] requires name and about text`); + } + try { + if ( + !isText(link?.url) || + !["https:", "http:"].includes(new URL(link.url).protocol) + ) + throw new Error(); + } catch { + errors.push( + `contact_links[${index}].url must be an absolute HTTP(S) URL`, + ); + } + }); + } + return errors; +} + +async function listFiles(directory, prefix = "") { + const files = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + if ([".git", "node_modules", ".worktrees"].includes(entry.name)) continue; + const relative = prefix + entry.name; + if (entry.isDirectory()) + files.push( + ...(await listFiles(path.join(directory, entry.name), relative + "/")), + ); + else if (entry.isFile()) files.push(relative); + } + return files; +} + +function* walk(node) { + yield node; + for (const child of node.children ?? []) yield* walk(child); +} + +async function validateLinks(source, file, root) { + const errors = []; + const nodes = [...walk(markdown.parse(source))]; + const definitions = new Map( + nodes + .filter((node) => node.type === "definition") + .map((node) => [node.identifier, node.url]), + ); + for (const node of nodes) { + if ( + !["link", "image", "linkReference", "imageReference"].includes(node.type) + ) + continue; + const url = node.url ?? definitions.get(node.identifier); + if (!url || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(url)) continue; + try { + const target = decodeURIComponent(url.split(/[?#]/, 1)[0]); + if (!target) continue; + const resolved = target.startsWith("/") + ? path.resolve(root, "." + target) + : path.resolve(root, path.dirname(file), target); + const relative = path.relative(root, resolved); + if ( + relative === ".." || + relative.startsWith(".." + path.sep) || + path.isAbsolute(relative) + ) { + throw new Error("target is outside this repository"); + } + await stat(resolved); + } catch { + errors.push( + `line ${node.position.start.line}: relative link target does not exist in this repository: ${url}`, + ); + } + } + return errors; +} + +export async function validateRepository(root) { + const errors = []; + const names = new Set(); + for (const file of await listFiles(root)) { + if (!/\.(?:md|ya?ml)$/i.test(file)) continue; + const source = await readFile(path.join(root, file), "utf8"); + let issues = []; + if (/\.md$/i.test(file)) { + issues = await validateLinks(source, file, root); + } else { + const document = parseDocument(source, { uniqueKeys: true }); + if (document.errors.length) { + issues = document.errors.map((error) => error.message); + } else if (file.startsWith(".github/ISSUE_TEMPLATE/")) { + const data = document.toJS(); + if (/\/config\.ya?ml$/.test(file)) issues = validateConfig(data); + else { + issues = validateForm(data); + if (isText(data?.name)) { + if (names.has(data.name)) + issues.push(`name duplicates another form: ${data.name}`); + names.add(data.name); + } + } + } + } + errors.push(...issues.map((issue) => `${file}: ${issue}`)); + } + return errors; +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href +) { + const errors = await validateRepository(process.cwd()); + if (errors.length) { + console.error(errors.join("\n")); + process.exitCode = 1; + } else { + console.log("Community forms, YAML and relative Markdown links are valid."); + } +} diff --git a/test/check-community.test.mjs b/test/check-community.test.mjs new file mode 100644 index 0000000..1748ad3 --- /dev/null +++ b/test/check-community.test.mjs @@ -0,0 +1,178 @@ +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { + validateForm, + validateConfig, + validateRepository, +} from "../scripts/check-community.mjs"; + +const form = () => ({ + name: "不具合", + description: "不具合の報告", + body: [ + { + type: "textarea", + id: "summary", + attributes: { label: "概要" }, + validations: { required: true }, + }, + ], +}); + +async function repository(t, files) { + const root = await mkdtemp(path.join(tmpdir(), "acecore-community-")); + t.after(() => rm(root, { recursive: true, force: true })); + for (const [file, content] of Object.entries(files)) { + await mkdir(path.dirname(path.join(root, file)), { recursive: true }); + await writeFile(path.join(root, file), content); + } + return root; +} + +test("accepts a valid form, including supported optional field types", () => { + const value = form(); + value.body.push( + { type: "markdown", attributes: { value: "説明" } }, + { type: "input", attributes: { label: "URL" } }, + { + type: "dropdown", + attributes: { label: "OS", options: ["Windows", "Linux"] }, + }, + { + type: "checkboxes", + attributes: { + label: "確認", + options: [{ label: "確認済み", required: true }], + }, + }, + { type: "upload", attributes: { label: "添付" } }, + ); + assert.deepEqual(validateForm(value), []); +}); + +test("rejects missing required keys, invalid fields and malformed validations", () => { + for (const key of ["name", "description", "body"]) { + const value = form(); + delete value[key]; + assert.ok(validateForm(value).some((error) => error.startsWith(key))); + } + for (const field of [ + null, + { type: "invalid" }, + { type: "input" }, + { type: "input", attributes: {} }, + ]) { + const value = form(); + value.body = [field]; + assert.ok(validateForm(value).length > 0); + } + const value = form(); + value.body[0].validations.required = "true"; + assert.match(validateForm(value).join("\n"), /must be a boolean/); +}); + +test("rejects duplicate or invalid IDs and duplicate labels", () => { + const value = form(); + value.body.push(structuredClone(value.body[0])); + assert.match(validateForm(value).join("\n"), /id duplicates/); + assert.match(validateForm(value).join("\n"), /label duplicates/); + value.body = [{ ...value.body[0], id: "not valid!" }]; + assert.match(validateForm(value).join("\n"), /id must contain/); +}); + +test("rejects markdown-only forms and invalid selection options", () => { + const value = form(); + value.body = [{ type: "markdown", attributes: { value: "説明" } }]; + assert.match(validateForm(value).join("\n"), /at least one input/); + for (const [type, options] of [ + ["dropdown", []], + ["dropdown", [false]], + ["checkboxes", [{ label: "確認", required: "yes" }]], + ]) { + value.body = [{ type, attributes: { label: "選択", options } }]; + assert.match(validateForm(value).join("\n"), /options/); + } +}); + +test("validates blank issue and contact link configuration", () => { + const value = { + blank_issues_enabled: true, + contact_links: [ + { + name: "Security", + about: "Read the policy", + url: "https://example.com/security", + }, + ], + }; + assert.deepEqual(validateConfig(value), []); + assert.ok(validateConfig({ blank_issues_enabled: "true" }).length > 0); + assert.ok(validateConfig({ contact_links: {} }).length > 0); + for (const link of [ + null, + {}, + { name: "Security", about: "Policy", url: "../SECURITY.md" }, + { name: "Security", about: "Policy", url: "javascript:alert(1)" }, + ]) { + assert.ok(validateConfig({ contact_links: [link] }).length > 0); + } +}); + +test("accepts existing nested, encoded, reference, image and root-relative links", async (t) => { + const root = await repository(t, { + "README.md": + "[Guide](docs/guide.md#heading)\n[File](docs/a%20b.md)\n[Reference][guide]\n\n[guide]: docs/guide.md\n\n![Image](docs/icon.svg)\n[Web](https://example.invalid/)\n`[Example](missing.md)`\n", + "docs/guide.md": + "[Home](../README.md)\n[Root](/README.md)\n[Section](#heading)\n", + "docs/a b.md": "# Text\n", + "docs/icon.svg": "\n", + "node_modules/example/broken.md": "[Missing](missing.md)\n", + ".github/ISSUE_TEMPLATE/bug.yml": JSON.stringify(form()), + ".github/ISSUE_TEMPLATE/config.yml": "blank_issues_enabled: true\n", + }); + assert.deepEqual(await validateRepository(root), []); +}); + +test("reports missing link targets, including references, with the source line", async (t) => { + const root = await repository(t, { + "README.md": + "[Broken](missing.md)\n[Ref][missing]\n\n[missing]: missing-too.md\n", + }); + const errors = await validateRepository(root); + assert.equal(errors.length, 2); + assert.match(errors[0], /README.md: line 1:.*missing.md/); + assert.match(errors[1], /README.md: line 2:.*missing-too.md/); +}); + +test("rejects paths escaping the repository and malformed percent encoding", async (t) => { + const root = await repository(t, { + "README.md": "[Outside](../file.md)\n[Malformed](bad%ZZ.md)\n", + }); + assert.equal((await validateRepository(root)).length, 2); +}); + +test("rejects malformed YAML and duplicate mapping keys in any YAML file", async (t) => { + const root = await repository(t, { + ".github/ISSUE_TEMPLATE/bug.yml": "body: [\n", + ".github/workflows/check.yml": "name: One\nname: Two\n", + }); + const errors = await validateRepository(root); + assert.equal(errors.length, 2); + assert.ok( + errors.some((error) => error.startsWith(".github/workflows/check.yml:")), + ); +}); + +test("rejects duplicate form names across files", async (t) => { + const root = await repository(t, { + ".github/ISSUE_TEMPLATE/one.yml": JSON.stringify(form()), + ".github/ISSUE_TEMPLATE/two.yml": JSON.stringify(form()), + }); + assert.match( + (await validateRepository(root)).join("\n"), + /name duplicates another form/, + ); +});