chore: rerun compliance checks #2
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR Compliance Reusable | ||
|
Check failure on line 1 in .github/workflows/pr-compliance-reusable.yml
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| pr-number: | ||
| required: true | ||
| type: number | ||
| pr-body: | ||
| required: false | ||
| default: "" | ||
| type: string | ||
| pr-author-login: | ||
| required: true | ||
| type: string | ||
| default-branch: | ||
| required: true | ||
| type: string | ||
| compliance-profile: | ||
| required: false | ||
| default: bsl-change-license-commercial | ||
| type: string | ||
| usage-license-confirmation-label: | ||
| required: false | ||
| default: "" | ||
| type: string | ||
| app-id: | ||
| required: false | ||
| default: "" | ||
| type: string | ||
| jobs: | ||
| validate-pr-metadata: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Create GitHub App token | ||
| id: app_token | ||
| if: ${{ inputs.app-id != '' && secrets.CLA_APP_PRIVATE_KEY != '' }} | ||
| uses: actions/create-github-app-token@v2 | ||
| with: | ||
| app-id: ${{ inputs.app-id }} | ||
| private-key: ${{ secrets.CLA_APP_PRIVATE_KEY }} | ||
| owner: ${{ github.repository_owner }} | ||
| - name: Resolve compliance profile | ||
| id: resolve_profile | ||
| uses: actions/github-script@v8 | ||
| with: | ||
| github-token: ${{ steps.app_token.outputs.token || secrets.CLA_BOT_TOKEN || secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const profiles = { | ||
| "bsl-change-license-commercial": { | ||
| usageLicenseConfirmationLabel: | ||
| "I understand accepted contributions may be used under the repository's documented BSL, change-license, and commercial licensing model." | ||
| }, | ||
| "apache-2.0": { | ||
| usageLicenseConfirmationLabel: | ||
| "I understand accepted contributions may be distributed under the repository's documented Apache 2.0 licensing and distribution model and the contributor agreement terms described in CLA.md." | ||
| }, | ||
| "mit": { | ||
| usageLicenseConfirmationLabel: | ||
| "I understand accepted contributions may be distributed under the repository's documented MIT licensing and distribution model and the contributor agreement terms described in CLA.md." | ||
| }, | ||
| "bsd-3-clause": { | ||
| usageLicenseConfirmationLabel: | ||
| "I understand accepted contributions may be distributed under the repository's documented BSD-3-Clause licensing and distribution model and the contributor agreement terms described in CLA.md." | ||
| }, | ||
| "gpl-2.0-or-later": { | ||
| usageLicenseConfirmationLabel: | ||
| "I understand accepted contributions may be distributed under the repository's documented GPL-2.0-or-later licensing and distribution model and the contributor agreement terms described in CLA.md." | ||
| }, | ||
| "gpl-3.0-or-later": { | ||
| usageLicenseConfirmationLabel: | ||
| "I understand accepted contributions may be distributed under the repository's documented GPL-3.0-or-later licensing and distribution model and the contributor agreement terms described in CLA.md." | ||
| } | ||
| }; | ||
| const profileName = process.env.COMPLIANCE_PROFILE?.trim() || "bsl-change-license-commercial"; | ||
| const profile = profiles[profileName]; | ||
| if (!profile) { | ||
| core.setFailed(`Unknown compliance profile: ${profileName}`); | ||
| return; | ||
| } | ||
| const usageLicenseConfirmationLabel = | ||
| process.env.USAGE_LICENSE_CONFIRMATION_LABEL_OVERRIDE?.trim() || | ||
| profile.usageLicenseConfirmationLabel; | ||
| core.setOutput("usage_license_confirmation_label", usageLicenseConfirmationLabel); | ||
| env: | ||
| COMPLIANCE_PROFILE: ${{ inputs.compliance-profile }} | ||
| USAGE_LICENSE_CONFIRMATION_LABEL_OVERRIDE: ${{ inputs.usage-license-confirmation-label }} | ||
| - name: Validate PR declarations | ||
| uses: actions/github-script@v8 | ||
| with: | ||
| github-token: ${{ steps.app_token.outputs.token || secrets.CLA_BOT_TOKEN || secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const prNumber = Number(${{ inputs.pr-number }}); | ||
| const body = ${{ toJson(inputs.pr-body) }}; | ||
| const prAuthorLogin = ${{ toJson(inputs.pr-author-login) }}; | ||
| const defaultBranch = ${{ toJson(inputs.default-branch) }}; | ||
| const marker = "<!-- golutra-pr-compliance -->"; | ||
| const issues = []; | ||
| const normalizeValue = (value) => value.trim().toLowerCase(); | ||
| const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | ||
| const isChecked = (label) => { | ||
| const pattern = new RegExp(`- \\[x\\] ${escapeRegExp(label)}`, "i"); | ||
| return pattern.test(body); | ||
| }; | ||
| const extractLine = (label) => { | ||
| const pattern = new RegExp(`^${escapeRegExp(label)}:\\s*(.+)$`, "mi"); | ||
| const match = body.match(pattern); | ||
| return match ? match[1].trim() : ""; | ||
| }; | ||
| const extractSection = (heading, nextHeading) => { | ||
| const nextPart = nextHeading ? `\\n## ${escapeRegExp(nextHeading)}` : "$"; | ||
| const pattern = new RegExp(`## ${escapeRegExp(heading)}\\s*\\n([\\s\\S]*?)${nextPart}`, "i"); | ||
| const match = body.match(pattern); | ||
| return match ? match[1].trim() : ""; | ||
| }; | ||
| const splitRegistryCell = (value) => | ||
| value | ||
| .split(",") | ||
| .map((item) => item.trim()) | ||
| .filter(Boolean); | ||
| const isEmptyRegistryValue = (value) => | ||
| !value || /^none\.?$/i.test(value) || /^n\/a$/i.test(value) || value === "-"; | ||
| const personalLabel = "This contribution is submitted in my personal capacity."; | ||
| const organizationLabel = "This contribution is submitted on behalf of an organization that already has a signed CCLA on file."; | ||
| const personalChecked = isChecked(personalLabel); | ||
| const organizationChecked = isChecked(organizationLabel); | ||
| if (personalChecked === organizationChecked) { | ||
| issues.push("必须且只能选择一种贡献身份:个人贡献或代表组织贡献。"); | ||
| } | ||
| const requiredLabels = [ | ||
| "I have the legal right to submit this contribution.", | ||
| ${{ toJson(steps.resolve_profile.outputs.usage_license_confirmation_label) }}, | ||
| "I have disclosed below any third-party, copied, or adapted code and the relevant source/license details.", | ||
| "I have reviewed any AI-assisted output included in this PR and confirmed that I have the right to submit it.", | ||
| "I understand that undisclosed or incompatible third-party code may cause this PR to be rejected or later removed." | ||
| ]; | ||
| for (const label of requiredLabels) { | ||
| if (!isChecked(label)) { | ||
| issues.push(`缺少必填确认项:${label}`); | ||
| } | ||
| } | ||
| const organizationName = extractLine("Organization name"); | ||
| const authorizationReference = extractLine("Authorization reference"); | ||
| const thirdPartyDisclosure = extractSection( | ||
| "Third-Party / Copied / Adapted Code Disclosure", | ||
| "AI Assistance Disclosure" | ||
| ); | ||
| const aiDisclosure = extractSection("AI Assistance Disclosure", "Co-author Disclosure"); | ||
| const coAuthorDisclosure = extractSection("Co-author Disclosure", ""); | ||
| const commitLogins = new Set([prAuthorLogin.toLowerCase()]); | ||
| const coAuthorEntries = []; | ||
| const loadCommitMetadata = async () => { | ||
| const commits = await github.paginate(github.rest.pulls.listCommits, { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: prNumber, | ||
| per_page: 100 | ||
| }); | ||
| const coAuthorPattern = /^Co-authored-by:\s*(.+?)\s*<([^>]+)>$/gim; | ||
| for (const commit of commits) { | ||
| if (commit.author?.login) { | ||
| commitLogins.add(commit.author.login.toLowerCase()); | ||
| } | ||
| const message = commit.commit?.message ?? ""; | ||
| let match = null; | ||
| while ((match = coAuthorPattern.exec(message)) !== null) { | ||
| coAuthorEntries.push({ | ||
| name: match[1].trim(), | ||
| email: match[2].trim().toLowerCase() | ||
| }); | ||
| } | ||
| } | ||
| }; | ||
| if (!thirdPartyDisclosure) { | ||
| issues.push("必须填写 Third-Party / Copied / Adapted Code Disclosure,若无请明确写 `None.`。"); | ||
| } | ||
| if (!aiDisclosure) { | ||
| issues.push("必须填写 AI Assistance Disclosure,若无请明确写 `None.`。"); | ||
| } | ||
| await loadCommitMetadata(); | ||
| if (coAuthorEntries.length === 0) { | ||
| if (coAuthorDisclosure && !/^none\.?$/i.test(coAuthorDisclosure)) { | ||
| issues.push("没有检测到 `Co-authored-by:`,`Co-author Disclosure` 应填写 `None.`。"); | ||
| } | ||
| } else if (!coAuthorDisclosure || /^none\.?$/i.test(coAuthorDisclosure)) { | ||
| issues.push( | ||
| "检测到 commit 中存在 `Co-authored-by:`,必须在 `Co-author Disclosure` 中列出所有共同作者。" | ||
| ); | ||
| } else { | ||
| const normalizedDisclosure = coAuthorDisclosure.toLowerCase(); | ||
| for (const coAuthor of coAuthorEntries) { | ||
| if (!normalizedDisclosure.includes(coAuthor.email)) { | ||
| issues.push(`Co-author Disclosure 缺少共同作者邮箱:${coAuthor.email}`); | ||
| } | ||
| } | ||
| } | ||
| if (organizationChecked) { | ||
| if (!organizationName || /^none\.?$/i.test(organizationName)) { | ||
| issues.push("代表组织贡献时,`Organization name` 不能为空。"); | ||
| } | ||
| if (!authorizationReference || /^none\.?$/i.test(authorizationReference)) { | ||
| issues.push("代表组织贡献时,`Authorization reference` 必须填写已登记的授权编号。"); | ||
| } else { | ||
| try { | ||
| const response = await github.rest.repos.getContent({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| path: "docs/legal/corporate-authorizations.json", | ||
| ref: defaultBranch | ||
| }); | ||
| const content = Buffer.from(response.data.content, "base64").toString("utf8"); | ||
| const parsedRegistry = JSON.parse(content); | ||
| const registry = Array.isArray(parsedRegistry) | ||
| ? parsedRegistry | ||
| : parsedRegistry.authorizations; | ||
| if (!Array.isArray(registry)) { | ||
| issues.push( | ||
| "企业授权登记文件格式无效:`docs/legal/corporate-authorizations.json` 缺少 `authorizations` 数组。" | ||
| ); | ||
| throw new Error("invalid corporate authorization registry"); | ||
| } | ||
| const matchedAuthorization = registry.find( | ||
| (entry) => normalizeValue(entry.authorizationReference) === normalizeValue(authorizationReference) | ||
| ); | ||
| if (!matchedAuthorization) { | ||
| issues.push( | ||
| `未在 docs/legal/corporate-authorizations.json 中找到授权编号:${authorizationReference}` | ||
| ); | ||
| } else { | ||
| if ( | ||
| normalizeValue(matchedAuthorization.organization) !== normalizeValue(organizationName) | ||
| ) { | ||
| issues.push( | ||
| `授权编号 ${authorizationReference} 对应的组织名与 PR 中填写的 \`Organization name\` 不一致。` | ||
| ); | ||
| } | ||
| if (normalizeValue(matchedAuthorization.status) !== "active") { | ||
| issues.push( | ||
| `授权编号 ${authorizationReference} 当前状态不是 Active,不能用于企业贡献。` | ||
| ); | ||
| } | ||
| if (!isEmptyRegistryValue(matchedAuthorization.expirationDate)) { | ||
| const expirationDate = Date.parse(matchedAuthorization.expirationDate); | ||
| if (!Number.isNaN(expirationDate) && expirationDate < Date.now()) { | ||
| issues.push(`授权编号 ${authorizationReference} 已过期。`); | ||
| } | ||
| } | ||
| const authorizedUsers = splitRegistryCell( | ||
| Array.isArray(matchedAuthorization.authorizedGitHubUsernames) | ||
| ? matchedAuthorization.authorizedGitHubUsernames.join(",") | ||
| : matchedAuthorization.authorizedGitHubUsernames ?? "" | ||
| ).map((item) => item.toLowerCase()); | ||
| const authorizedEmails = splitRegistryCell( | ||
| Array.isArray(matchedAuthorization.authorizedEmails) | ||
| ? matchedAuthorization.authorizedEmails.join(",") | ||
| : matchedAuthorization.authorizedEmails ?? "" | ||
| ).map((item) => item.toLowerCase()); | ||
| const wildcardAllowed = authorizedUsers.includes("*"); | ||
| const wildcardEmailAllowed = authorizedEmails.includes("*"); | ||
| if (!wildcardAllowed && authorizedUsers.length === 0) { | ||
| issues.push( | ||
| `授权编号 ${authorizationReference} 没有登记任何 Authorized GitHub Usernames。` | ||
| ); | ||
| } | ||
| if (!wildcardAllowed) { | ||
| const unauthorizedLogins = [...commitLogins].filter( | ||
| (login) => !authorizedUsers.includes(login) | ||
| ); | ||
| if (unauthorizedLogins.length > 0) { | ||
| issues.push( | ||
| `授权编号 ${authorizationReference} 未覆盖这些 GitHub 用户:${unauthorizedLogins.join(", ")}` | ||
| ); | ||
| } | ||
| } | ||
| if (coAuthorEntries.length > 0 && !wildcardEmailAllowed) { | ||
| if (authorizedEmails.length === 0) { | ||
| issues.push( | ||
| `授权编号 ${authorizationReference} 检测到共同作者,但没有登记任何 Authorized Emails。` | ||
| ); | ||
| } else { | ||
| const unauthorizedCoAuthorEmails = coAuthorEntries | ||
| .map((coAuthor) => coAuthor.email) | ||
| .filter((email) => !authorizedEmails.includes(email)); | ||
| if (unauthorizedCoAuthorEmails.length > 0) { | ||
| issues.push( | ||
| `授权编号 ${authorizationReference} 未覆盖这些共同作者邮箱:${unauthorizedCoAuthorEmails.join(", ")}` | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } catch (error) { | ||
| if (error.message !== "invalid corporate authorization registry") { | ||
| issues.push("无法读取企业授权登记表,请确认 docs/legal/corporate-authorizations.json 存在且可访问。"); | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| if (organizationName && !/^none\.?$/i.test(organizationName)) { | ||
| issues.push("个人贡献时,`Organization name` 应填写 `None`。"); | ||
| } | ||
| if (authorizationReference && !/^none\.?$/i.test(authorizationReference)) { | ||
| issues.push("个人贡献时,`Authorization reference` 应填写 `None`。"); | ||
| } | ||
| } | ||
| const commentBody = issues.length === 0 | ||
| ? `${marker} | ||
| PR 合规检查已通过。 | ||
| - 贡献身份声明完整 | ||
| - 协议与使用方式确认完整 | ||
| - 第三方来源、AI 与共同作者披露字段已填写` | ||
| : `${marker} | ||
| PR 合规检查未通过,请按模板补齐以下内容: | ||
| ${issues.map((issue) => `- ${issue}`).join("\n")}`; | ||
| const comments = await github.paginate(github.rest.issues.listComments, { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: prNumber | ||
| }); | ||
| const existingComment = comments.find((comment) => | ||
| comment.user?.type === "Bot" && comment.body?.includes(marker) | ||
| ); | ||
| if (existingComment) { | ||
| await github.rest.issues.updateComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: existingComment.id, | ||
| body: commentBody | ||
| }); | ||
| } else { | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: prNumber, | ||
| body: commentBody | ||
| }); | ||
| } | ||
| if (issues.length > 0) { | ||
| core.setFailed(issues.join("\n")); | ||
| } | ||