mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +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: |
|
value: |
|
||||||
Thanks for filing this report. Keep every answer concise, reproducible, and grounded in observed evidence.
|
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`.
|
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
|
- type: dropdown
|
||||||
id: bug_type
|
id: bug_type
|
||||||
attributes:
|
attributes:
|
||||||
@@ -20,6 +22,19 @@ body:
|
|||||||
- Behavior bug (incorrect output/state without crash)
|
- Behavior bug (incorrect output/state without crash)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
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
|
- type: textarea
|
||||||
id: summary
|
id: summary
|
||||||
attributes:
|
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:
|
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:
|
- Problem:
|
||||||
- Why it matters:
|
- Why it matters:
|
||||||
- What changed:
|
- What changed:
|
||||||
|
|||||||
160
.github/workflows/labeler.yml
vendored
160
.github/workflows/labeler.yml
vendored
@@ -2,9 +2,9 @@ name: Labeler
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned triage workflow; no untrusted checkout or PR code execution
|
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:
|
issues:
|
||||||
types: [opened]
|
types: [opened, edited]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
max_prs:
|
max_prs:
|
||||||
@@ -209,6 +209,59 @@ jobs:
|
|||||||
// labels: [trustedLabel],
|
// 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
|
- name: Apply too-many-prs label
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
@@ -419,6 +472,7 @@ jobs:
|
|||||||
const maxCount = processAll ? Number.POSITIVE_INFINITY : Math.max(1, maxPrs);
|
const maxCount = processAll ? Number.POSITIVE_INFINITY : Math.max(1, maxPrs);
|
||||||
|
|
||||||
const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"];
|
const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"];
|
||||||
|
const betaBlockerLabel = "beta-blocker";
|
||||||
const labelColor = "b76e79";
|
const labelColor = "b76e79";
|
||||||
// const trustedLabel = "trusted-contributor";
|
// const trustedLabel = "trusted-contributor";
|
||||||
// const experiencedLabel = "experienced-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) {
|
async function resolveContributorLabel(login) {
|
||||||
if (contributorCache.has(login)) {
|
if (contributorCache.has(login)) {
|
||||||
return contributorCache.get(login);
|
return contributorCache.get(login);
|
||||||
@@ -580,7 +650,37 @@ jobs:
|
|||||||
labelNames.add(label);
|
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();
|
await ensureSizeLabels();
|
||||||
|
const betaBlockerLabelExists = await hasBetaBlockerLabel();
|
||||||
|
|
||||||
let page = 1;
|
let page = 1;
|
||||||
let processed = 0;
|
let processed = 0;
|
||||||
@@ -618,6 +718,9 @@ jobs:
|
|||||||
|
|
||||||
await applySizeLabel(pullRequest, currentLabels, labelNames);
|
await applySizeLabel(pullRequest, currentLabels, labelNames);
|
||||||
await applyContributorLabel(pullRequest, labelNames);
|
await applyContributorLabel(pullRequest, labelNames);
|
||||||
|
if (betaBlockerLabelExists) {
|
||||||
|
await applyBetaBlockerTitleLabel(pullRequest, labelNames);
|
||||||
|
}
|
||||||
|
|
||||||
processed += 1;
|
processed += 1;
|
||||||
}
|
}
|
||||||
@@ -719,3 +822,56 @@ jobs:
|
|||||||
// labels: [trustedLabel],
|
// 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.
|
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.
|
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.
|
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.
|
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. Silence means green. If you miss the window, your fix likely lands in the next cycle.
|
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
|
## Next steps
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { resolve } from "node:path";
|
|||||||
type RepoLabel = {
|
type RepoLabel = {
|
||||||
name: string;
|
name: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLOR_BY_PREFIX = new Map<string, string>([
|
const COLOR_BY_PREFIX = new Map<string, string>([
|
||||||
@@ -17,8 +18,31 @@ const COLOR_BY_PREFIX = new Map<string, string>([
|
|||||||
["size", "fbca04"],
|
["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 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 = [
|
const labelNames = [
|
||||||
...new Set([...extractLabelNames(readFileSync(configPath, "utf8")), ...EXTRA_LABELS]),
|
...new Set([...extractLabelNames(readFileSync(configPath, "utf8")), ...EXTRA_LABELS]),
|
||||||
];
|
];
|
||||||
@@ -37,12 +61,21 @@ if (!missing.length) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const label of missing) {
|
for (const label of missing) {
|
||||||
const color = pickColor(label);
|
const metadata = resolveLabelMetadata(label);
|
||||||
execFileSync(
|
const args = [
|
||||||
"gh",
|
"api",
|
||||||
["api", "-X", "POST", `repos/${repo}/labels`, "-f", `name=${label}`, "-f", `color=${color}`],
|
"-X",
|
||||||
{ stdio: "inherit" },
|
"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}`);
|
console.log(`Created label: ${label}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,9 +99,13 @@ function extractLabelNames(contents: string): string[] {
|
|||||||
return labels;
|
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();
|
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 {
|
function resolveRepo(): string {
|
||||||
|
|||||||
@@ -9,9 +9,17 @@ async function waitForPersistedSecret(configPath: string, expectedSecret: string
|
|||||||
const deadline = Date.now() + 3_000;
|
const deadline = Date.now() + 3_000;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const raw = await fs.readFile(configPath, "utf-8");
|
const raw = await fs.readFile(configPath, "utf-8");
|
||||||
const parsed = JSON.parse(raw) as {
|
let parsed: {
|
||||||
commands?: { ownerDisplaySecret?: string };
|
commands?: { ownerDisplaySecret?: string };
|
||||||
};
|
};
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(raw) as {
|
||||||
|
commands?: { ownerDisplaySecret?: string };
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
await sleep(5);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (parsed.commands?.ownerDisplaySecret === expectedSecret) {
|
if (parsed.commands?.ownerDisplaySecret === expectedSecret) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user