From ce6abbab3bb8515f2b78552a07d868b013014df1 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 10 May 2024 20:03:03 -0700 Subject: [PATCH] update contributing --- CONTRIBUTING.md | 89 ++++++++++++++++++++++++++++- eng/common/labels.yaml | 7 +-- eng/common/scripts/sync-labels.ts | 93 ++++++++++++++++++++++++++----- 3 files changed, 170 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e38f80b8d5..bcbffeb008 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -222,7 +222,94 @@ As a contributor you can run the following command to trigger the build and crea /azp run typespec - pr tools ``` -# Labels for issues and PRs +# Issue and Pr processes + +## Labels + +TypeSpec repo use labels to help categorize and manage issues and PRs. The following is a list of labels and their descriptions. + + + + +#### issue_kinds + +Issue kinds + +| Name | Color | Description | +| --------- | ------- | ------------------------------------------ | +| `bug` | #cccccc | Something isn't working | +| `feature` | #cccccc | New feature or request | +| `docs` | #cccccc | Improvements or additions to documentation | +| `epic` | #cccccc | | + +#### area + +Area of the codebase + +| Name | Color | Description | +| ---------------------------- | ------- | ----------------------------------- | +| `compiler:core` | #453261 | Issues for @typespec/compiler | +| `compiler:emitter-framework` | #453261 | Issues for the emitter framework | +| `ide` | #846da1 | Issues for VS, VSCode, Monaco, etc. | +| `lib:http` | #c7aee6 | | +| `lib:openapi` | #c7aee6 | | +| `lib:rest` | #c7aee6 | | +| `lib:versioning` | #c7aee6 | | +| `meta:blog` | #007dc8 | Blog updates | +| `meta:website` | #007dc8 | TypeSpec.io updates | +| `Service Codegen` | #7C4B1E | | +| `tspd` | #004185 | Issues for the tspd tool | +| `emitter:client:csharp` | #e1b300 | | +| `emitter:json-schema` | #957300 | | +| `emitter:protobuf` | #957300 | The protobuf emitter | +| `emitter:service:csharp` | #967200 | | +| `emitter:service:js` | #967200 | | +| `eng` | #65bfff | | + +#### breaking-change + +Labels around annotating issues and PR if they contain breaking change or deprecation + +| Name | Color | Description | +| ----------------- | ------- | ---------------------------------------------------------------------------------- | +| `breaking-change` | #B60205 | A change that might cause specs or code to break | +| `deprecation` | #760205 | A previously supported feature will now report a warning and eventually be removed | + +#### design-issues + +Design issue management + +| Name | Color | Description | +| ----------------- | ------- | ------------------------------------------------------ | +| `design:accepted` | #1a4421 | Proposal for design has been discussed and accepted. | +| `design:needed` | #96c499 | A design request has been raised that needs a proposal | +| `design:proposed` | #56815a | Proposal has been added and ready for discussion | + +#### process + +Process labels + +| Name | Color | Description | +| -------------- | ------- | --------------------------------------------------------------------------------- | +| `needs-triage` | #ffffff | | +| `needs-info` | #ffffff | Mark an issue that needs reply from the author or it will be closed automatically | +| `triaged:core` | #5319e7 | | + +#### misc + +Misc labels + +| Name | Color | Description | +| -------------------------- | ------- | ------------------ | +| `Client Emitter Migration` | #FD92F0 | | +| `good first issue` | #7057ff | Good for newcomers | + + + +### Updating labels + +Labels are configured in `eng/common/labels.yaml`. To update labels, edit this file and run `pnpm sync-labels`. +**If you create a new label in github UI without updating the `labels.yaml` file, it WILL be automatically removed** # TypeSpec Emitters diff --git a/eng/common/labels.yaml b/eng/common/labels.yaml index bf8fe48e07..35dac379c0 100644 --- a/eng/common/labels.yaml +++ b/eng/common/labels.yaml @@ -12,7 +12,9 @@ issue_kinds: docs: color: cccccc description: Improvements or additions to documentation - + epic: + color: cccccc + description: "" area: description: "Area of the codebase" labels: @@ -49,9 +51,6 @@ area: tspd: color: "004185" description: Issues for the tspd tool - epic: - color: cccccc - description: "" emitter:client:csharp: color: e1b300 description: "" diff --git a/eng/common/scripts/sync-labels.ts b/eng/common/scripts/sync-labels.ts index 48fa46b6e2..0303f25ecd 100644 --- a/eng/common/scripts/sync-labels.ts +++ b/eng/common/scripts/sync-labels.ts @@ -1,12 +1,19 @@ import { Octokit } from "@octokit/rest"; -import { readFile } from "fs/promises"; +import { readFile, writeFile } from "fs/promises"; import { dirname, resolve } from "path"; import pc from "picocolors"; +import { format, resolveConfig } from "prettier"; import { fileURLToPath } from "url"; import { inspect, parseArgs } from "util"; import { parse } from "yaml"; - -const labelFile = resolve(dirname(fileURLToPath(import.meta.url)), "../labels.yaml"); +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const labelFileRelative = "eng/common/labels.yaml"; +const labelFile = resolve(repoRoot, labelFileRelative); +const contributingFile = resolve(repoRoot, "CONTRIBUTING.md"); +const magicComment = { + start: "", + end: "", +} as const; const repo = { owner: "microsoft", @@ -46,6 +53,8 @@ async function main() { if (options.values["update-github-labels"]) { await updateGithubLabels(labels.labels, { dryRun: options.values["dry-run"] }); } + + updateContributingFile(labels, { dryRun: options.values["dry-run"] }); } function loadLabels(yamlContent: string): LabelsConfig { @@ -97,11 +106,11 @@ function prettyLabel(label: Label, padEnd: number = 0) { return `${pc.cyan(label.name.padEnd(padEnd))} ${pc.blue(`#${label.color}`)} ${pc.gray(label.description)}`; } -interface UpdateGithubLabelOptions { +interface ActionOptions { readonly dryRun?: boolean; } -async function updateGithubLabels(labels: Label[], options: UpdateGithubLabelOptions = {}) { +async function updateGithubLabels(labels: Label[], options: ActionOptions = {}) { if (!options.dryRun && !process.env.GITHUB_TOKEN) { throw new Error( "GITHUB_TOKEN environment variable is required when not running in dry-run mode" @@ -149,22 +158,18 @@ async function fetchAllLabels(octokit: Octokit) { return result; } -function logAction(message: string, options: UpdateGithubLabelOptions) { +function logAction(message: string, options: ActionOptions) { const prefix = options.dryRun ? `${pc.gray("[dry-run]")} ` : ""; console.log(prefix + message); } -async function doAction( - action: () => Promise, - label: string, - options: UpdateGithubLabelOptions -) { +async function doAction(action: () => Promise, label: string, options: ActionOptions) { if (!options.dryRun) { await action(); } logAction(label, options); } -async function createLabels(octokit: Octokit, labels: Label[], options: UpdateGithubLabelOptions) { +async function createLabels(octokit: Octokit, labels: Label[], options: ActionOptions) { for (const label of labels) { await doAction( () => octokit.rest.issues.createLabel({ ...repo, ...label }), @@ -173,7 +178,7 @@ async function createLabels(octokit: Octokit, labels: Label[], options: UpdateGi ); } } -async function updateLabels(octokit: Octokit, labels: Label[], options: UpdateGithubLabelOptions) { +async function updateLabels(octokit: Octokit, labels: Label[], options: ActionOptions) { for (const label of labels) { await doAction( () => octokit.rest.issues.updateLabel({ ...repo, ...label }), @@ -182,7 +187,7 @@ async function updateLabels(octokit: Octokit, labels: Label[], options: UpdateGi ); } } -async function deleteLabels(octokit: Octokit, labels: string[], options: UpdateGithubLabelOptions) { +async function deleteLabels(octokit: Octokit, labels: string[], options: ActionOptions) { for (const name of labels) { await doAction( () => octokit.rest.issues.deleteLabel({ ...repo, name }), @@ -204,3 +209,63 @@ function validateLabel(label: Label) { throw new Error(`Label missing description: ${inspect(label)}`); } } + +async function updateContributingFile(labels: LabelsConfig, options: ActionOptions) { + console.log("Updating contributing file", contributingFile); + const content = await readFile(contributingFile, "utf8"); + const startIndex = content.indexOf(magicComment.start); + const endIndex = content.indexOf(magicComment.end); + if (startIndex === -1) { + throw new Error(`Could not find start comment "${magicComment.start}" in ${contributingFile}`); + } + const start = content.slice(0, startIndex + magicComment.start.length); + const end = + endIndex === -1 + ? magicComment.end + "\n" + content.slice(startIndex + magicComment.start.length) + : content.slice(endIndex); + + const warning = ``; + const newContent = `${start}\n${warning}\n${generateLabelsDoc(labels)}\n${end}`; + const prettierOptions = await resolveConfig(contributingFile); + const formatted = await format(newContent, { ...prettierOptions, filepath: contributingFile }); + await doAction( + () => writeFile(contributingFile, formatted), + "Updated contributing file", + options + ); +} + +function generateLabelsDoc(labels: LabelsConfig) { + return [ + "### Labels reference", + ...labels.categories.map((category) => { + return `#### ${category.name}\n\n${category.description}\n\n${table([ + ["Name", "Color", "Description"], + ...category.labels.map((label) => [ + inlinecode(label.name), + `#${label.color}`, + label.description, + ]), + ])}`; + }), + ].join("\n"); +} + +// #region markdown helpers +export function inlinecode(code: string) { + return "`" + code + "`"; +} +function escapeMarkdownTable(text: string) { + return text.replace(/([^\\])(\|)/g, "$1\\$2").replace(/\n/g, "
"); +} + +function table([header, ...rows]: string[][]) { + const renderRow = (row: string[]): string => `| ${row.map(escapeMarkdownTable).join(" | ")} |`; + + return [ + renderRow(header), + "|" + header.map((x) => "-".repeat(x.length + 2)).join("|") + "|", + ...rows.map(renderRow), + ].join("\n"); +} +// #endregion