[Bug]: GitHub's Moderate repository role cannot change pull request labels
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Aptitud para principiantes
- 67/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- github, typescript
Línea de trabajo
Empieza con toCanTriage en packages/source-control-github/src/server/gitHubPullRequestJson.ts y sigue su uso en toViewerPermissions; el script de reproducción del issue muestra los casos fallidos MODERATE y TRIAGE_PLUS. Revisa las pruebas de permisos pertinentes y añade cobertura para ambos roles. Confirma que canWrite sigue siendo false y que se permiten las etiquetas. El trabajo está terminado cuando el mapper de permisos y las pruebas coincidan con el comportamiento esperado en el issue.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
Use a checkout of the commit under Version or commit with its repository-pinned dependencies installed and Node available. Save the script below outside the checkout and run it with sh from anywhere inside the checkout. It needs no GitHub credential and makes no remote changes. It writes one probe file to a mktemp directory with a private HOME, TMPDIR and T3CODE_HOME, removes it on exit, and leaves the checkout unchanged.
- The probe decodes a structurally complete GraphQL result through the generated production
ViewerPermissionsresult schema, the same decoderGitHubPullRequestApi.getViewerAccessuses, for eachviewerPermissionvalueREAD,TRIAGE,TRIAGE_PLUS,WRITE,MAINTAIN,ADMIN,nullandMODERATE. - It passes each decoded result to the exported production
toViewerPermissionsand then to the productiongitHubViewerPermissionsmapper, and printscanWrite,canTriage,canDeleteBranch,labelsandactionsper value. - Compare the
TRIAGE,TRIAGE_PLUSandMODERATErows. The script exits 0 only whenTRIAGE_PLUSandMODERATEdecode but getlabels: false, and the controls hold:READis refused labels,TRIAGE,WRITE,MAINTAIN,ADMINandnullare granted labels, andtoCanWrite(undefined)is false.
The corresponding user path is to open a pull request in a project whose server GitHub credential holds the Moderate repository role and use the label control. That path was not exercised: no account assigned that role was used, and no browser, mobile client or complete server RPC ran.
Reproduction script: repro.sh
#!/bin/sh
set -eu
checkout=$(git rev-parse --show-toplevel)
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
mkdir -p "$tmp/fakehome" "$tmp/tmp" "$tmp/t3home"
export HOME="$tmp/fakehome" TMPDIR="$tmp/tmp" T3CODE_HOME="$tmp/t3home"
export T3_VERIFY_CHECKOUT="$checkout"
cat > "$tmp/probe.mts" <<'TS'
import { pathToFileURL } from "node:url";
const root = process.env.T3_VERIFY_CHECKOUT!;
const load = (path: string) => import(pathToFileURL(`${root}/${path}`).href);
const { createRequire } = await import("node:module");
const require = createRequire(`${root}/packages/source-control-github/package.json`);
const Schema = await import(pathToFileURL(require.resolve("effect/Schema")).href);
const { ViewerPermissions } = await load("packages/source-control-github/src/server/pullRequestReads.graphql.ts");
const { toViewerPermissions, toCanWrite } = await load("packages/source-control-github/src/server/gitHubPullRequestJson.ts");
const { gitHubViewerPermissions } = await load("packages/source-control-github/src/server/GitHubPullRequestProvider.ts");
function fixture(viewerPermission: string | null) {
return { repository: {
__typename: "Repository", viewerPermission,
mergeCommitAllowed: true, squashMergeAllowed: true,
rebaseMergeAllowed: true, autoMergeAllowed: false,
pullRequest: { __typename: "PullRequest", viewerCanUpdate: false,
viewerDidAuthor: false, headRepository: { __typename: "Repository", viewerPermission } },
} };
}
const rows = ["READ", "TRIAGE", "TRIAGE_PLUS", "WRITE", "MAINTAIN", "ADMIN", null, "MODERATE"].map(permission => {
const payload = fixture(permission);
let decoded;
try { decoded = Schema.decodeUnknownSync(ViewerPermissions.result)(payload); }
catch (e) { return { permission, decode: "rejected", error: String(e) }; }
const access = toViewerPermissions(decoded);
if (access === null) throw new Error("repository unexpectedly absent");
const viewer = gitHubViewerPermissions(access);
return { permission, decode: "accepted", canWrite: access.canWrite,
canTriage: access.canTriage, canDeleteBranch: access.canDeleteBranch,
labels: viewer.labels, actions: viewer.actions,
webLabelEditorAllowed: viewer.labels !== false,
mobileLabelToggleAllowedWhenConnectedAndEnvironmentAllowed: viewer.labels !== false };
});
for (const row of rows) console.log(JSON.stringify(row));
console.log("toCanWrite(undefined)=" + toCanWrite(undefined));
const row = (p: string | null) => rows.find(r => r.permission === p)!;
const controls = row("READ").labels === false && row("TRIAGE").labels === true
&& ["WRITE", "MAINTAIN", "ADMIN", null].every(p => row(p).labels === true)
&& toCanWrite(undefined) === false;
const defect = row("TRIAGE_PLUS").decode === "accepted" && row("TRIAGE_PLUS").labels === false;
const moderateDefect = row("MODERATE").decode === "accepted" && row("MODERATE").labels === false;
console.log(`controls=${controls}; TRIAGE_PLUS label defect=${defect}; MODERATE label defect=${moderateDefect}`);
process.exit(controls && defect && moderateDefect ? 0 : 1);
TS
cd "$checkout/packages/source-control-github"
env -u NODE_ENV node "$tmp/probe.mts"
Expected behavior
A repository role that includes triage abilities keeps permission to apply and dismiss PR labels in T3 Code, as TRIAGE already does. GitHub's repository roles table grants "Apply/dismiss labels" at the triage level and above. The repository's vendored GitHub schema describes TRIAGE_PLUS as triage plus additional abilities. GitHub's currently served GraphQL schema lists MODERATE, describes it as able to "moderate and edit issues and manage pull requests without write access", and marks TRIAGE_PLUS deprecated with the reason "TRIAGE_PLUS has been renamed to MODERATE. Use MODERATE instead. Removal on 2027-04-01 UTC." GitHub's REST API for adding a repository collaborator accepts moderate alongside pull, triage, push, maintain and admin on organization-owned repositories, separately from custom repository role names, so this is a documented built-in repository permission, not necessarily an Enterprise custom role. With the script's input, the MODERATE and TRIAGE_PLUS rows should report canTriage: true and labels: true while keeping canWrite: false.
Actual behavior
Summary. T3 Code decides whether the GitHub credential can apply and remove pull request labels from the repository's viewerPermission. That mapping grants label editing to TRIAGE and to the write-level roles, but not to MODERATE, GitHub's current repository permission that includes triage abilities, nor to TRIAGE_PLUS, its deprecated former name. Both values decode successfully and both end with canTriage: false and labels: false, while TRIAGE gets true. As a result, when the server's GitHub credential holds the Moderate role, users see the PR label control as unavailable with a triage-access explanation (web) or notice (mobile), and the server refuses label candidate reads and label writes. The effect is limited to PR labels: merging, branch deletion and reviewer requests use other permissions, and canWrite: false is correct for both roles. This is a gap in the original triage comparison from the labels feature, not a regression.
In the script's output, TRIAGE produces canTriage: true and labels: true, while TRIAGE_PLUS and MODERATE produce false for both; all three correctly report canWrite: false. The toCanTriage comparison accepts only the exact TRIAGE spelling or a writing role. toViewerPermissions passes that result on, and the provider maps it to labels: access.canTriage. The PR detail read reaches the same comparison through toPullRequestCore.
The downstream effects are established by reading the consumers, not by running them. Web passes the false flag to the label picker, which renders an unavailable button with a triage-access tooltip. Mobile disables label toggles and shows a triage-access notice. The server refuses label candidate reads and refuses label writes when the mapped permission is false.
Evidence
- Expected source: GitHub's repository roles table (
data/tables/repository-roles.yml, "Apply/dismiss labels" granted at triage and above), GitHub's served GraphQLRepositoryPermissionenum (MODERATE, withTRIAGE_PLUSdeprecated as renamed to it), the vendored schema atpackages/source-control-github/schema/github.graphql.tslines 190-199, the REST collaborator and team repository-permission schemas that acceptmoderate, and the passingTRIAGEcontrol in the reproduction. - Failure source: the observed reproduction output under Logs or stack traces, and the comparison in
toCanTriageatpackages/source-control-github/src/server/gitHubPullRequestJson.tslines 1524-1538, which does not match either role name. - Evidence provenance: observed
- Local verification: reproduced
- Reproduction completeness: incomplete
- Missing fact: an actual
repository.viewerPermissionresponse from a GitHub account assigned the Moderate role; without it, whether github.com still emits the deprecatedTRIAGE_PLUSspelling and the plan or rollout limits on Moderate are unconfirmed.
The defect itself does not depend on the missing fact: the production decoder accepts MODERATE and the mapper denies labels for it. The pinned Effect GraphQL enum decoding accepts unknown future strings, so MODERATE being absent from the vendored enum does not stop it before toCanTriage. To inspect the current external contract read-only, run gh api graphql -f 'query={ __type(name: "RepositoryPermission") { enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } } }'.
History: #19095 and its fix #19103 made an explicit null permission (GitHub App tokens) grant write and triage; that change did not address either role name. The triage comparison dates from the original labels feature in #9313, which treated triage as the least role allowed to label. Open #30510 and #32774 change label-picker interaction and latency, not this mapping. GitLab and Forgejo use host-provided booleans rather than this role ladder, so this mapping does not apply to them.
The script's output on the tested commit is under Logs or stack traces.
Restoration check
With the script's input, the MODERATE and TRIAGE_PLUS rows report canTriage: true and labels: true while canWrite, canDeleteBranch and actions stay false, false and []. As controls, READ is still refused labels, TRIAGE, WRITE, MAINTAIN, ADMIN and null are still granted labels, and toCanWrite(undefined) stays false. The script asserts the current failing results, so its TRIAGE_PLUS and MODERATE expectations need flipping for this check.
Impact
Minor bug or occasional failure
A documented GitHub repository permission that includes triage abilities is consistently mapped to no PR label access, so label editing in T3 Code fails for every PR when the credential holds that role. It is limited to labels and to that role; real-world prevalence and rollout scope were not measured.
Version or commit
d9805b2ca667be7caec8c9035950564ab96704c3, rerun from a clean checkout; the same script also exits 0 on main at 3ffd38ef7f67439eaaf17d2ffc1a71ee7cf4f33f; the toCanTriage comparison is unchanged on main at 6807c3fb865d3f9fcc0679b2e283cfd6064a4ea7 by source inspection only
Environment
Darwin arm64, Node v26.12.1, repository-pinned dependencies; production GraphQL decoder and permission mappers invoked directly, with no live GitHub write, role assignment, browser, mobile client or server
Logs or stack traces
{"permission":"READ","decode":"accepted","canWrite":false,"canTriage":false,"canDeleteBranch":false,"labels":false,"actions":[],"webLabelEditorAllowed":false,"mobileLabelToggleAllowedWhenConnectedAndEnvironmentAllowed":false}
{"permission":"TRIAGE","decode":"accepted","canWrite":false,"canTriage":true,"canDeleteBranch":false,"labels":true,"actions":[],"webLabelEditorAllowed":true,"mobileLabelToggleAllowedWhenConnectedAndEnvironmentAllowed":true}
{"permission":"TRIAGE_PLUS","decode":"accepted","canWrite":false,"canTriage":false,"canDeleteBranch":false,"labels":false,"actions":[],"webLabelEditorAllowed":false,"mobileLabelToggleAllowedWhenConnectedAndEnvironmentAllowed":false}
{"permission":"WRITE","decode":"accepted","canWrite":true,"canTriage":true,"canDeleteBranch":true,"labels":true,"actions":["delete-branch","merge","disable-auto-merge","revert","approve-workflows"],"webLabelEditorAllowed":true,"mobileLabelToggleAllowedWhenConnectedAndEnvironmentAllowed":true}
{"permission":"MAINTAIN","decode":"accepted","canWrite":true,"canTriage":true,"canDeleteBranch":true,"labels":true,"actions":["delete-branch","merge","disable-auto-merge","revert","approve-workflows"],"webLabelEditorAllowed":true,"mobileLabelToggleAllowedWhenConnectedAndEnvironmentAllowed":true}
{"permission":"ADMIN","decode":"accepted","canWrite":true,"canTriage":true,"canDeleteBranch":true,"labels":true,"actions":["delete-branch","merge","disable-auto-merge","revert","approve-workflows"],"webLabelEditorAllowed":true,"mobileLabelToggleAllowedWhenConnectedAndEnvironmentAllowed":true}
{"permission":null,"decode":"accepted","canWrite":true,"canTriage":true,"canDeleteBranch":true,"labels":true,"actions":["delete-branch","merge","disable-auto-merge","revert","approve-workflows"],"webLabelEditorAllowed":true,"mobileLabelToggleAllowedWhenConnectedAndEnvironmentAllowed":true}
{"permission":"MODERATE","decode":"accepted","canWrite":false,"canTriage":false,"canDeleteBranch":false,"labels":false,"actions":[],"webLabelEditorAllowed":false,"mobileLabelToggleAllowedWhenConnectedAndEnvironmentAllowed":false}
toCanWrite(undefined)=false
controls=true; TRIAGE_PLUS label defect=true; MODERATE label defect=true
Screenshots, recordings, or supporting files
No response
Workaround
Apply or dismiss the labels on GitHub directly with the same role. GitHub's documented permission table supports this, but no live label change was made to confirm it.
- Lenguaje dominante
- TypeScript
- Estrellas
- 24.8k
- Forks
- 6.4k
- Merge medio
- 4 h 16 min
- PR fusionados (30 d)
- 352
Preparar el entorno
Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de pingdotgg/t3code
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
[Bug]: t3_thread_read drops the typed failure (usage_limit class and resetAt) on error itemsAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Los mantenedores suelen responder en 1 día
Todos los issues de pingdotgg/t3code
Issues similares
-
Add "append" and "prepend" slots for vue-vuetify extended renderersPosiblemente ocupada @simeyc la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
eclipsesource/jsonforms#2668 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
cobot/zapier#520 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
Los mantenedores suelen responder en 1 día
-
README の通知条件を実際の判定処理に合わせるPosiblemente ocupada @unhappychoice la tomó hoy. Abiertofactory:in-progress priority:medium
Dificultad 1/5 Menos de una hora Aptitud para principiantes 90/100
unhappychoice/oss-issue-opener#395 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
bug Website
Dificultad 2/5 1-3 horas Aptitud para principiantes 77/100
OWASP/OWASP-Hunting#88 ·
Los mantenedores suelen responder en 1 día