mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 01:11:16 +07:00
docs: add beta blocker contributor guidance (#55199)
* docs: add beta blocker contributor guidance * fix: tighten beta blocker labeling and flaky config test
This commit is contained in:
15
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
15
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -9,6 +9,8 @@ body:
|
||||
value: |
|
||||
Thanks for filing this report. Keep every answer concise, reproducible, and grounded in observed evidence.
|
||||
Do not speculate or infer beyond the evidence. If a narrative section cannot be answered from the available evidence, respond with exactly `NOT_ENOUGH_INFO`.
|
||||
|
||||
If this is a plugin beta-release blocker, rename the issue title to `Beta blocker: <plugin-name> - <summary>` and apply the `beta-blocker` label after filing.
|
||||
- type: dropdown
|
||||
id: bug_type
|
||||
attributes:
|
||||
@@ -20,6 +22,19 @@ body:
|
||||
- Behavior bug (incorrect output/state without crash)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: beta_blocker
|
||||
attributes:
|
||||
label: Beta release blocker
|
||||
description: >
|
||||
Choose `Yes` only if this blocks plugin compatibility during the current beta release window.
|
||||
Selecting `Yes` does not apply the label automatically. You must also rename the issue title
|
||||
to `Beta blocker: <plugin-name> - <summary>` for the automation to apply the `beta-blocker` label.
|
||||
options:
|
||||
- "No"
|
||||
- "Yes"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -2,6 +2,8 @@
|
||||
|
||||
Describe the problem and fix in 2–5 bullets:
|
||||
|
||||
If this PR fixes a plugin beta-release blocker, title it `fix(<plugin-id>): beta blocker - <summary>` and link the matching `Beta blocker: <plugin-name> - <summary>` issue labeled `beta-blocker`. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation.
|
||||
|
||||
- Problem:
|
||||
- Why it matters:
|
||||
- What changed:
|
||||
|
||||
160
.github/workflows/labeler.yml
vendored
160
.github/workflows/labeler.yml
vendored
@@ -2,9 +2,9 @@ name: Labeler
|
||||
|
||||
on:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned triage workflow; no untrusted checkout or PR code execution
|
||||
types: [opened, synchronize, reopened]
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
issues:
|
||||
types: [opened]
|
||||
types: [opened, edited]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
max_prs:
|
||||
@@ -209,6 +209,59 @@ jobs:
|
||||
// labels: [trustedLabel],
|
||||
// });
|
||||
// }
|
||||
- name: Apply beta-blocker title label
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
|
||||
script: |
|
||||
const pullRequest = context.payload.pull_request;
|
||||
if (!pullRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelName = "beta-blocker";
|
||||
const matchesBetaBlocker = /\bbeta blocker\b/i.test(pullRequest.title ?? "");
|
||||
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: labelName,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error?.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
core.info(`Skipping ${labelName} labeling because the label does not exist in the repository.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
per_page: 100,
|
||||
});
|
||||
const hasLabel = currentLabels.some((label) => label.name === labelName);
|
||||
|
||||
if (matchesBetaBlocker && !hasLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
labels: [labelName],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!matchesBetaBlocker && hasLabel) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
name: labelName,
|
||||
});
|
||||
}
|
||||
- name: Apply too-many-prs label
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
@@ -419,6 +472,7 @@ jobs:
|
||||
const maxCount = processAll ? Number.POSITIVE_INFINITY : Math.max(1, maxPrs);
|
||||
|
||||
const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"];
|
||||
const betaBlockerLabel = "beta-blocker";
|
||||
const labelColor = "b76e79";
|
||||
// const trustedLabel = "trusted-contributor";
|
||||
// const experiencedLabel = "experienced-contributor";
|
||||
@@ -449,6 +503,22 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
async function hasBetaBlockerLabel() {
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner,
|
||||
repo,
|
||||
name: betaBlockerLabel,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error?.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveContributorLabel(login) {
|
||||
if (contributorCache.has(login)) {
|
||||
return contributorCache.get(login);
|
||||
@@ -580,7 +650,37 @@ jobs:
|
||||
labelNames.add(label);
|
||||
}
|
||||
|
||||
async function applyBetaBlockerTitleLabel(pullRequest, labelNames) {
|
||||
const matchesBetaBlocker = /\bbeta blocker\b/i.test(pullRequest.title ?? "");
|
||||
|
||||
if (matchesBetaBlocker) {
|
||||
if (!labelNames.has(betaBlockerLabel)) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullRequest.number,
|
||||
labels: [betaBlockerLabel],
|
||||
});
|
||||
labelNames.add(betaBlockerLabel);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!labelNames.has(betaBlockerLabel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullRequest.number,
|
||||
name: betaBlockerLabel,
|
||||
});
|
||||
labelNames.delete(betaBlockerLabel);
|
||||
}
|
||||
|
||||
await ensureSizeLabels();
|
||||
const betaBlockerLabelExists = await hasBetaBlockerLabel();
|
||||
|
||||
let page = 1;
|
||||
let processed = 0;
|
||||
@@ -618,6 +718,9 @@ jobs:
|
||||
|
||||
await applySizeLabel(pullRequest, currentLabels, labelNames);
|
||||
await applyContributorLabel(pullRequest, labelNames);
|
||||
if (betaBlockerLabelExists) {
|
||||
await applyBetaBlockerTitleLabel(pullRequest, labelNames);
|
||||
}
|
||||
|
||||
processed += 1;
|
||||
}
|
||||
@@ -719,3 +822,56 @@ jobs:
|
||||
// labels: [trustedLabel],
|
||||
// });
|
||||
// }
|
||||
- name: Apply beta-blocker title label
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
if (!issue || issue.pull_request) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelName = "beta-blocker";
|
||||
const matchesBetaBlocker = /^beta blocker:/i.test(issue.title ?? "");
|
||||
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: labelName,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error?.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
core.info(`Skipping ${labelName} labeling because the label does not exist in the repository.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
per_page: 100,
|
||||
});
|
||||
const hasLabel = currentLabels.some((label) => label.name === labelName);
|
||||
|
||||
if (matchesBetaBlocker && !hasLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: [labelName],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!matchesBetaBlocker && hasLabel) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
name: labelName,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,8 +229,9 @@ internal imports — never import your own plugin through its SDK path.
|
||||
1. Watch for GitHub release tags on [openclaw/openclaw](https://github.com/openclaw/openclaw/releases) and subscribe via `Watch` > `Releases`. Beta tags look like `v2026.3.N-beta.1`. You can also turn on notifications for the official OpenClaw X account [@openclaw](https://x.com/openclaw) for release announcements.
|
||||
2. Test your plugin against the beta tag as soon as it appears. The window before stable is typically only a few hours.
|
||||
3. Post in your plugin's thread in the `plugin-forum` Discord channel after testing with either `all good` or what broke. If you do not have a thread yet, create one.
|
||||
4. If something breaks, ship a fix PR to `main` and drop the link in your thread. Blockers with a PR get merged; blockers without one might ship anyway. Maintainers watch these threads during beta testing.
|
||||
5. Silence means green. If you miss the window, your fix likely lands in the next cycle.
|
||||
4. If something breaks, open or update an issue titled `Beta blocker: <plugin-name> - <summary>` and apply the `beta-blocker` label. Put the issue link in your thread.
|
||||
5. Open a PR to `main` titled `fix(<plugin-id>): beta blocker - <summary>` and link the issue in both the PR and your Discord thread. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation. Blockers with a PR get merged; blockers without one might ship anyway. Maintainers watch these threads during beta testing.
|
||||
6. Silence means green. If you miss the window, your fix likely lands in the next cycle.
|
||||
|
||||
## Next steps
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { resolve } from "node:path";
|
||||
type RepoLabel = {
|
||||
name: string;
|
||||
color?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
const COLOR_BY_PREFIX = new Map<string, string>([
|
||||
@@ -17,8 +18,31 @@ const COLOR_BY_PREFIX = new Map<string, string>([
|
||||
["size", "fbca04"],
|
||||
]);
|
||||
|
||||
const EXTRA_LABEL_METADATA = new Map<
|
||||
string,
|
||||
{
|
||||
color: string;
|
||||
description?: string;
|
||||
}
|
||||
>([
|
||||
[
|
||||
"beta-blocker",
|
||||
{
|
||||
color: "D93F0B",
|
||||
description: "Plugin beta-release blocker pending stable cutoff triage",
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const configPath = resolve(".github/labeler.yml");
|
||||
const EXTRA_LABELS = ["size: XS", "size: S", "size: M", "size: L", "size: XL"] as const;
|
||||
const EXTRA_LABELS = [
|
||||
"size: XS",
|
||||
"size: S",
|
||||
"size: M",
|
||||
"size: L",
|
||||
"size: XL",
|
||||
"beta-blocker",
|
||||
] as const;
|
||||
const labelNames = [
|
||||
...new Set([...extractLabelNames(readFileSync(configPath, "utf8")), ...EXTRA_LABELS]),
|
||||
];
|
||||
@@ -37,12 +61,21 @@ if (!missing.length) {
|
||||
}
|
||||
|
||||
for (const label of missing) {
|
||||
const color = pickColor(label);
|
||||
execFileSync(
|
||||
"gh",
|
||||
["api", "-X", "POST", `repos/${repo}/labels`, "-f", `name=${label}`, "-f", `color=${color}`],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
const metadata = resolveLabelMetadata(label);
|
||||
const args = [
|
||||
"api",
|
||||
"-X",
|
||||
"POST",
|
||||
`repos/${repo}/labels`,
|
||||
"-f",
|
||||
`name=${label}`,
|
||||
"-f",
|
||||
`color=${metadata.color}`,
|
||||
];
|
||||
if (metadata.description) {
|
||||
args.push("-f", `description=${metadata.description}`);
|
||||
}
|
||||
execFileSync("gh", args, { stdio: "inherit" });
|
||||
console.log(`Created label: ${label}`);
|
||||
}
|
||||
|
||||
@@ -66,9 +99,13 @@ function extractLabelNames(contents: string): string[] {
|
||||
return labels;
|
||||
}
|
||||
|
||||
function pickColor(label: string): string {
|
||||
function resolveLabelMetadata(label: string): { color: string; description?: string } {
|
||||
const extraMetadata = EXTRA_LABEL_METADATA.get(label);
|
||||
if (extraMetadata) {
|
||||
return extraMetadata;
|
||||
}
|
||||
const prefix = label.includes(":") ? label.split(":", 1)[0].trim() : label.trim();
|
||||
return COLOR_BY_PREFIX.get(prefix) ?? "ededed";
|
||||
return { color: COLOR_BY_PREFIX.get(prefix) ?? "ededed" };
|
||||
}
|
||||
|
||||
function resolveRepo(): string {
|
||||
|
||||
@@ -9,9 +9,17 @@ async function waitForPersistedSecret(configPath: string, expectedSecret: string
|
||||
const deadline = Date.now() + 3_000;
|
||||
while (Date.now() < deadline) {
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
let parsed: {
|
||||
commands?: { ownerDisplaySecret?: string };
|
||||
};
|
||||
try {
|
||||
parsed = JSON.parse(raw) as {
|
||||
commands?: { ownerDisplaySecret?: string };
|
||||
};
|
||||
} catch {
|
||||
await sleep(5);
|
||||
continue;
|
||||
}
|
||||
if (parsed.commands?.ownerDisplaySecret === expectedSecret) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user