diff --git a/.github/scripts/cla/manage-label.mjs b/.github/scripts/cla/manage-label.mjs new file mode 100644 index 00000000000..a899f1fc4dd --- /dev/null +++ b/.github/scripts/cla/manage-label.mjs @@ -0,0 +1,83 @@ +// Invoked from .github/workflows/ci-cla-check.yml via actions/github-script. +// +// Adds the `cla-signed` label when every contributor has signed, and +// removes it otherwise. Idempotent: re-runs safely without duplicating +// the label or erroring if it's already in the desired state. Creates +// the label on first use so the workflow is self-contained. + +/** + * @typedef { InstanceType } GitHubInstance + * @typedef { import("@actions/github/lib/context").Context } Context + * @typedef { typeof import("@actions/core") } Core + */ + +const LABEL_NAME = 'cla-signed'; +const LABEL_COLOR = '0e8a16'; // GitHub's standard green +const LABEL_DESCRIPTION = 'All contributors on this PR have signed the CLA'; + +/** + * @param {{ github: GitHubInstance, context: Context, core: Core }} params + */ +export default async function manageClaLabel({ github, context, core }) { + const { owner, repo } = context.repo; + const issue_number = Number(process.env.PR_NUMBER); + const allSigned = process.env.ALL_SIGNED === 'true'; + + if (allSigned) { + // Make sure the label exists before trying to apply it — addLabels + // errors if the label is missing from the repo. + try { + await github.rest.issues.getLabel({ owner, repo, name: LABEL_NAME }); + } catch (e) { + if (errorStatus(e) === 404) { + try { + await github.rest.issues.createLabel({ + owner, + repo, + name: LABEL_NAME, + color: LABEL_COLOR, + description: LABEL_DESCRIPTION, + }); + } catch (createErr) { + // 422 = race with a parallel run that just created it. Fine. + if (errorStatus(createErr) !== 422) throw createErr; + } + } else { + throw e; + } + } + + await github.rest.issues.addLabels({ + owner, + repo, + issue_number, + labels: [LABEL_NAME], + }); + core.info(`Applied "${LABEL_NAME}" label to PR #${issue_number}`); + } else { + // 404 just means the label wasn't on the PR — nothing to undo. + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number, + name: LABEL_NAME, + }); + core.info(`Removed "${LABEL_NAME}" label from PR #${issue_number}`); + } catch (e) { + if (errorStatus(e) !== 404) throw e; + } + } +} + +/** + * Octokit's request errors carry an HTTP `status` field, but TypeScript + * sees catch parameters as `unknown`. This guard narrows safely. + * @param {unknown} e + * @returns {number | undefined} + */ +function errorStatus(e) { + return typeof e === 'object' && e !== null && 'status' in e && typeof e.status === 'number' + ? e.status + : undefined; +} diff --git a/.github/scripts/cla/update-pr-comment.mjs b/.github/scripts/cla/update-pr-comment.mjs index 50b87da0b2c..5a16bca25e8 100644 --- a/.github/scripts/cla/update-pr-comment.mjs +++ b/.github/scripts/cla/update-pr-comment.mjs @@ -51,9 +51,7 @@ export default async function updatePRComment({ github, context }) { } else { const lines = [MARKER, '## CLA signatures required', '']; lines.push(`Thank you for your submission! We really appreciate it. -Like many open source projects, we ask that you sign our [Contributor License Agreement](${process.env.CLA_SIGN_URL}) before we can accept your contribution. - -After signing, please comment \`\`\`/cla-check\`\`\` to re-check signature status.`); +Like many open source projects, we ask that you sign our [Contributor License Agreement](${process.env.CLA_SIGN_URL}) before we can accept your contribution.`); lines.push(''); if (unsigned.length > 0) { diff --git a/.github/workflows/ci-cla-check.yml b/.github/workflows/ci-cla-check.yml index 9d233d038d1..37448dfd8fe 100644 --- a/.github/workflows/ci-cla-check.yml +++ b/.github/workflows/ci-cla-check.yml @@ -148,6 +148,22 @@ jobs: const mod = await import('${{ github.workspace }}/.github/scripts/cla/update-pr-comment.mjs'); await mod.default({ github, context, core }); + - name: Manage cla-signed label + # Skip on merge_group (no PR) and when the check produced no result. + if: >- + always() && + steps.context.outputs.pr_number != '' && + steps.check.outputs.all_signed != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + PR_NUMBER: ${{ steps.context.outputs.pr_number }} + ALL_SIGNED: ${{ steps.check.outputs.all_signed }} + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const mod = await import('${{ github.workspace }}/.github/scripts/cla/manage-label.mjs'); + await mod.default({ github, context, core }); + - name: React to /cla-check comment if: always() && github.event_name == 'issue_comment' && steps.check.outputs.all_signed != '' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8