Skip to content

Commit 64c3fca

Browse files
accesswatchclaude
andauthored
feat: enforce the accessibility report and credit rules (#277)
Two workflows for Open Source Accessibility Framework, Community actions 2 and 4. accessibility-reports.yml acknowledges each report, reminds the accessibility owner after 5 business days without a maintainer reply, reopens a report closed without an outcome, and invites the reporter to confirm a fix. accessibility-credits.yml asks each outside contributor once how they want to be credited and records their own answer as a label; a11y-credits.mjs prints the credits block, now a step in the quarterly review issue's checklist. The logic is .github/scripts/a11y-community.mjs, identical across the Community Access repositories; its tests run in npm run test:automation. Framework progress is unchanged. Claude-Session: https://claude.ai/code/session_01PmRBrUTuhzYnQBFxUCN6Vr Co-authored-by: Claude <noreply@anthropic.com>
1 parent 047073a commit 64c3fca

10 files changed

Lines changed: 1387 additions & 2 deletions

‎.github/a11y-community.json‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"projectName": "GIT Going with GitHub",
3+
"accessibilityLabel": "accessibility",
4+
"installDate": "2026-10-29",
5+
"owner": "accesswatch",
6+
"accessibilityDoc": "https://github.com/Community-Access/git-going-with-github/blob/main/ACCESSIBILITY.md",
7+
"responseBusinessDays": 5,
8+
"creditReminderDays": 14
9+
}

‎.github/scripts/__tests__/a11y-community.test.mjs‎

Lines changed: 472 additions & 0 deletions
Large diffs are not rendered by default.

‎.github/scripts/a11y-community.mjs‎

Lines changed: 640 additions & 0 deletions
Large diffs are not rendered by default.

‎.github/scripts/a11y-credits.mjs‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env node
2+
/**
3+
* a11y-credits.mjs - print the release-notes block of accessibility credits.
4+
*
5+
* node .github/scripts/a11y-credits.mjs --since <tag-or-YYYY-MM-DD> [--repo owner/name]
6+
*
7+
* Lists items labelled "credit: yes" or "credit: anonymous" that closed since
8+
* the tag or date, as the contributor chose to be credited, grouped as code
9+
* (pull requests) and other contributions (reports and other issues). Each
10+
* line describes the work by the item's title, never the person.
11+
*
12+
* The block goes to standard output, ready to paste. Warnings about credit
13+
* questions unanswered for more than 14 days go to standard error.
14+
*
15+
* Needs a token with read access: GITHUB_TOKEN or GH_TOKEN, or a signed-in
16+
* GitHub CLI (`gh auth token`). No dependencies.
17+
*/
18+
19+
import { execFileSync } from 'node:child_process';
20+
import path from 'node:path';
21+
import { fileURLToPath } from 'node:url';
22+
import {
23+
collectCredits, formatCredits, formatWarnings, loadConfig, makeClient, resolveSince,
24+
} from './a11y-community.mjs';
25+
26+
const HERE = path.dirname(fileURLToPath(import.meta.url));
27+
28+
function arg(name) {
29+
const i = process.argv.indexOf(name);
30+
return i === -1 ? null : process.argv[i + 1];
31+
}
32+
33+
function token() {
34+
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
35+
if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
36+
try {
37+
return execFileSync('gh', ['auth', 'token'], { encoding: 'utf8' }).trim();
38+
} catch {
39+
return '';
40+
}
41+
}
42+
43+
function repoFromGit() {
44+
const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: HERE, encoding: 'utf8' }).trim();
45+
const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/);
46+
if (!m) throw new Error(`cannot tell the repository from ${url}; pass --repo owner/name`);
47+
return `${m[1]}/${m[2]}`;
48+
}
49+
50+
/** request and paginate over fetch, matching what makeClient expects. */
51+
function fetchTransport(auth) {
52+
const headers = {
53+
accept: 'application/vnd.github+json',
54+
'x-github-api-version': '2022-11-28',
55+
'user-agent': 'a11y-credits',
56+
...(auth ? { authorization: `Bearer ${auth}` } : {}),
57+
};
58+
const build = (route, params) => {
59+
const [method, template] = route.split(' ');
60+
const rest = { ...params };
61+
const pathname = template.replace(/\{(\w+)\}/g, (_, key) => {
62+
const value = rest[key];
63+
delete rest[key];
64+
return encodeURIComponent(value);
65+
});
66+
const url = new URL(`https://api.github.com${pathname}`);
67+
for (const [key, value] of Object.entries(rest)) url.searchParams.set(key, value);
68+
return { method, url };
69+
};
70+
const call = async (method, url) => {
71+
const res = await fetch(url, { method, headers });
72+
if (!res.ok) {
73+
const error = new Error(`${method} ${url.pathname}: ${res.status} ${await res.text()}`);
74+
error.status = res.status;
75+
throw error;
76+
}
77+
return res;
78+
};
79+
return {
80+
request: async (route, params) => {
81+
const { method, url } = build(route, params);
82+
return (await call(method, url)).json();
83+
},
84+
paginate: async (route, params) => {
85+
let { method, url } = build(route, params);
86+
const all = [];
87+
while (url) {
88+
const res = await call(method, url);
89+
all.push(...(await res.json()));
90+
const next = (res.headers.get('link') || '').match(/<([^>]+)>;\s*rel="next"/);
91+
url = next ? new URL(next[1]) : null;
92+
}
93+
return all;
94+
},
95+
};
96+
}
97+
98+
async function main() {
99+
const sinceArg = arg('--since');
100+
if (!sinceArg) {
101+
console.error('Usage: node .github/scripts/a11y-credits.mjs --since <tag-or-YYYY-MM-DD> [--repo owner/name]');
102+
process.exit(2);
103+
}
104+
const fullName = arg('--repo') || repoFromGit();
105+
const [owner, repo] = fullName.split('/');
106+
const config = loadConfig(path.join(HERE, '..', 'a11y-community.json'));
107+
const client = makeClient(fetchTransport(token()), { owner, repo });
108+
109+
const since = await resolveSince(client, sinceArg);
110+
const result = await collectCredits(client, config, since);
111+
const block = formatCredits(result, config);
112+
if (block) process.stdout.write(block);
113+
else console.error(`No accessibility contributions to credit since ${since.toISOString().slice(0, 10)}.`);
114+
const warnings = formatWarnings(result, config);
115+
if (warnings) console.error(`\n${warnings}`);
116+
}
117+
118+
main().catch((error) => {
119+
console.error(error.message);
120+
process.exit(1);
121+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Accessibility credits
2+
3+
# Rule 2 of this project's accessibility practice (ACCESSIBILITY.md,
4+
# recognition; Open Source Accessibility Framework, Community action 4).
5+
#
6+
# - A merged pull request with the accessibility label, or one that closes an
7+
# accessibility issue, from someone who is not a maintainer or a bot; and an
8+
# accessibility report closed as completed with a documented outcome: one
9+
# question asking the contributor whether and how to be credited, label
10+
# "credit: asked". Never asked twice on the same item.
11+
# - The contributor, and only the contributor, replies "credit: @handle",
12+
# "credit: name <Name>", "credit: anonymous" or "credit: no": the label
13+
# becomes "credit: yes", "credit: anonymous" or "credit: no", with a short
14+
# confirmation. A chosen name is stored only in that confirmation.
15+
# - At release time, node .github/scripts/a11y-credits.mjs --since <tag-or-date>
16+
# prints the credits block for the release notes.
17+
#
18+
# pull_request_target is used so the question can be posted on pull requests
19+
# from forks. It checks out only the base branch's .github folder and never
20+
# runs code from the pull request.
21+
22+
on:
23+
pull_request_target:
24+
types: [closed]
25+
issues:
26+
types: [closed]
27+
issue_comment:
28+
types: [created]
29+
30+
# pull-requests: write is needed only because GitHub requires it to comment on,
31+
# and label, a pull request; issues: write alone is enough for issues.
32+
permissions:
33+
contents: read
34+
issues: write
35+
pull-requests: write
36+
37+
concurrency:
38+
group: accessibility-credits-${{ github.event.issue.number || github.event.pull_request.number }}
39+
cancel-in-progress: false
40+
41+
jobs:
42+
credit:
43+
# The label name repeats accessibilityLabel in .github/a11y-community.json.
44+
if: >-
45+
github.repository_owner == 'Community-Access' && (
46+
(github.event_name == 'pull_request_target' && github.event.pull_request.merged) ||
47+
(github.event_name == 'issues' && github.event.sender.type != 'Bot' &&
48+
contains(github.event.issue.labels.*.name, 'accessibility')) ||
49+
(github.event_name == 'issue_comment' && github.event.sender.type != 'Bot' &&
50+
contains(join(github.event.issue.labels.*.name, ','), 'credit: ')))
51+
runs-on: ubuntu-latest
52+
timeout-minutes: 5
53+
steps:
54+
- name: Check out the rule
55+
uses: actions/checkout@v4
56+
with:
57+
persist-credentials: false
58+
sparse-checkout: .github
59+
60+
- name: Apply the rule
61+
uses: actions/github-script@v8
62+
with:
63+
script: |
64+
const { runFromAction } = await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/a11y-community.mjs`);
65+
await runFromAction({ github, context, core }, 'credits');
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Accessibility reports
2+
3+
# Rule 1 of this project's accessibility practice (ACCESSIBILITY.md, "How we
4+
# respond"; Open Source Accessibility Framework, Community action 2). An
5+
# accessibility report is an issue with the accessibility label, opened by
6+
# someone who is not a maintainer or a bot, on or after the install date in
7+
# .github/a11y-community.json.
8+
#
9+
# - Opened or first labelled: one acknowledgement, label "a11y: awaiting response".
10+
# - A maintainer comments: that label and "a11y: response overdue" come off.
11+
# - Daily: still awaiting after 5 business days gets "a11y: response overdue"
12+
# and one reminder to the accessibility owner.
13+
# - Closed: needs a merged pull request or commit, or a maintainer comment with
14+
# an "Outcome:" line after the reporter's last comment; otherwise it is
15+
# reopened. Closed as completed invites the reporter to confirm the fix.
16+
#
17+
# The logic, and its tests, are in .github/scripts/a11y-community.mjs. Events
18+
# from bots are skipped, so the workflow's own reopen and comments never
19+
# trigger it again.
20+
21+
on:
22+
issues:
23+
types: [opened, labeled, closed]
24+
issue_comment:
25+
types: [created]
26+
schedule:
27+
# Daily at 13:41 UTC: 06:41 in Arizona, off the top of the hour.
28+
- cron: "41 13 * * *"
29+
30+
permissions:
31+
contents: read
32+
issues: write
33+
pull-requests: read
34+
35+
concurrency:
36+
group: accessibility-reports-${{ github.event.issue.number || 'overdue-sweep' }}
37+
cancel-in-progress: false
38+
39+
jobs:
40+
respond:
41+
# The label name repeats accessibilityLabel in .github/a11y-community.json,
42+
# so a comment on an unrelated issue does not start a job.
43+
if: >-
44+
github.repository_owner == 'Community-Access' && (
45+
github.event_name == 'schedule' ||
46+
(github.event.sender.type != 'Bot' &&
47+
github.event.issue.pull_request == null &&
48+
contains(github.event.issue.labels.*.name, 'accessibility')))
49+
runs-on: ubuntu-latest
50+
timeout-minutes: 5
51+
steps:
52+
- name: Check out the rule
53+
uses: actions/checkout@v4
54+
with:
55+
persist-credentials: false
56+
sparse-checkout: .github
57+
58+
- name: Apply the rule
59+
uses: actions/github-script@v8
60+
with:
61+
script: |
62+
const { runFromAction } = await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/a11y-community.mjs`);
63+
await runFromAction({ github, context, core }, 'reports');

‎.github/workflows/accessibility-review-reminder.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
'- [ ] Check that open reports have an acknowledgement, a severity, a status and a next step.',
6868
'- [ ] Keep a few `good first issue` and `help wanted` accessibility issues open, with context and acceptance criteria.',
6969
'- [ ] Review the dependency list in ACCESSIBILITY.md and any upstream blockers.',
70-
'- [ ] Credit accessibility contributors in the update, only in the form each person agreed to.',
70+
'- [ ] Credit accessibility contributors in the update, only in the form each person agreed to: run `node .github/scripts/a11y-credits.mjs --since ' + sinceDate + '` and paste its block; leave out anyone it lists as unanswered after 14 days, or ask them again.',
7171
'- [ ] Check the matching actions in the public Open Source Accessibility Framework tracking issue.',
7272
'',
7373
'Close this issue with a link to the pull request that adds the update.'

‎ACCESSIBILITY.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ We use four levels, based on how much the barrier stops a person completing a ta
9292
- Before we close a report, we write down the outcome on the issue, and we ask the reporter, or another person affected, to confirm the fix works for them when they are willing.
9393
- We credit reporters and testers only in the way they choose (see [Recognizing contributions](#recognizing-contributions)).
9494

95+
Two workflows hold us to this for reports opened from 29 October 2026 on. Their logic and its tests are in [.github/scripts/a11y-community.mjs](https://github.com/Community-Access/git-going-with-github/blob/main/.github/scripts/a11y-community.mjs).
96+
97+
- **When you report a barrier,** a bot replies once: it thanks you, says a maintainer will reply with a status and a next step within 5 business days, points you to [Known limitations](#known-limitations), and invites, but never requires, details of your setup. The issue is labeled `a11y: awaiting response` until a maintainer comments.
98+
- **If no maintainer has replied after 5 business days** (Monday to Friday), the issue is labeled `a11y: response overdue` and the accessibility owner is reminded on the issue.
99+
- **A report cannot close without an outcome.** Maintainers close it from the pull request or commit that fixes it, or write a comment with a line that starts `Outcome:`, after the reporter's last comment, saying what changed or why it cannot. A report closed without either is reopened automatically, with a note saying how to add one. When a report closes as fixed, you are invited to confirm that the fix works for you, and the issue is labeled `a11y: validation requested`.
100+
95101
If we cannot meet a date we gave, we say why on the issue, share any workaround, and give a new date.
96102

97103
## Taking part
@@ -193,6 +199,8 @@ We publish accessibility progress here, every quarter and after each cohort, and
193199

194200
Accessibility work, including reports, testing, reviews and documentation, is project work, and we say so publicly. Before we name anyone in a progress update, release notes or a community post, we ask how they would like to be credited: by GitHub handle, by name, or not at all, and we record the answer on the issue or pull request. We describe the work, never the person's disability, diagnosis or assistive technology, even if they mentioned it in a report, and we never present one person as speaking for a disability community.
195201

202+
A workflow does the asking. When an accessibility pull request from outside the maintainers is merged, or your report closes as fixed, a bot asks you once how you would like to be credited, and you reply with one line: `credit: @handle`, `credit: name <Name>`, `credit: anonymous` or `credit: no`. Only your own reply counts, you can change it later, and nothing about you beyond the credit you choose is published. The answer is recorded as a `credit:` label. Before each progress update, the accessibility owner runs `node .github/scripts/a11y-credits.mjs --since <date of the last update>` and pastes the block it prints; it lists items labeled `credit: yes` or `credit: anonymous` closed since then, as code and other contributions, and warns about `credit: asked` items with no answer after 14 days, whose contributors are left out or asked again.
203+
196204
## Reusing our accessibility materials
197205

198206
Other projects are welcome to reuse what we built for this workshop, under the repository's [CC BY 4.0 license](https://github.com/Community-Access/git-going-with-github/blob/main/LICENSE), with attribution. Each item below says what problem it solves and where it falls short.

‎html/ACCESSIBILITY.html‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,12 @@ <h3 id="how-we-respond">How we respond</h3>
311311
<li>Before we close a report, we write down the outcome on the issue, and we ask the reporter, or another person affected, to confirm the fix works for them when they are willing.</li>
312312
<li>We credit reporters and testers only in the way they choose (see <a href="#recognizing-contributions">Recognizing contributions</a>).</li>
313313
</ul>
314+
<p>Two workflows hold us to this for reports opened from 29 October 2026 on. Their logic and its tests are in <a href="https://github.com/Community-Access/git-going-with-github/blob/main/.github/scripts/a11y-community.mjs">.github/scripts/a11y-community.mjs</a>.</p>
315+
<ul>
316+
<li><strong>When you report a barrier,</strong> a bot replies once: it thanks you, says a maintainer will reply with a status and a next step within 5 business days, points you to <a href="#known-limitations">Known limitations</a>, and invites, but never requires, details of your setup. The issue is labeled <code>a11y: awaiting response</code> until a maintainer comments.</li>
317+
<li><strong>If no maintainer has replied after 5 business days</strong> (Monday to Friday), the issue is labeled <code>a11y: response overdue</code> and the accessibility owner is reminded on the issue.</li>
318+
<li><strong>A report cannot close without an outcome.</strong> Maintainers close it from the pull request or commit that fixes it, or write a comment with a line that starts <code>Outcome:</code>, after the reporter&#39;s last comment, saying what changed or why it cannot. A report closed without either is reopened automatically, with a note saying how to add one. When a report closes as fixed, you are invited to confirm that the fix works for you, and the issue is labeled <code>a11y: validation requested</code>.</li>
319+
</ul>
314320
<p>If we cannot meet a date we gave, we say why on the issue, share any workaround, and give a new date.</p>
315321
<h2 id="taking-part">Taking part</h2>
316322
<p>There are several ways to take part, and no single channel is required:</p>
@@ -400,6 +406,7 @@ <h3 id="october-2026-adopting-the-framework">October 2026: adopting the framewor
400406
</ul>
401407
<h2 id="recognizing-contributions">Recognizing contributions</h2>
402408
<p>Accessibility work, including reports, testing, reviews and documentation, is project work, and we say so publicly. Before we name anyone in a progress update, release notes or a community post, we ask how they would like to be credited: by GitHub handle, by name, or not at all, and we record the answer on the issue or pull request. We describe the work, never the person&#39;s disability, diagnosis or assistive technology, even if they mentioned it in a report, and we never present one person as speaking for a disability community.</p>
409+
<p>A workflow does the asking. When an accessibility pull request from outside the maintainers is merged, or your report closes as fixed, a bot asks you once how you would like to be credited, and you reply with one line: <code>credit: @handle</code>, <code>credit: name &lt;Name&gt;</code>, <code>credit: anonymous</code> or <code>credit: no</code>. Only your own reply counts, you can change it later, and nothing about you beyond the credit you choose is published. The answer is recorded as a <code>credit:</code> label. Before each progress update, the accessibility owner runs <code>node .github/scripts/a11y-credits.mjs --since &lt;date of the last update&gt;</code> and pastes the block it prints; it lists items labeled <code>credit: yes</code> or <code>credit: anonymous</code> closed since then, as code and other contributions, and warns about <code>credit: asked</code> items with no answer after 14 days, whose contributors are left out or asked again.</p>
403410
<h2 id="reusing-our-accessibility-materials">Reusing our accessibility materials</h2>
404411
<p>Other projects are welcome to reuse what we built for this workshop, under the repository&#39;s <a href="https://github.com/Community-Access/git-going-with-github/blob/main/LICENSE">CC BY 4.0 license</a>, with attribution. Each item below says what problem it solves and where it falls short.</p>
405412
<ul>

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"build": "npm run build:podcasts && npm run build:html",
5959
"watch:html": "node scripts/build-html.js --watch",
6060
"clean": "node -e \"require('fs').rmSync('html', {recursive: true, force: true})\"",
61-
"test:automation": "node --test .github/scripts/__tests__/*.test.js",
61+
"test:automation": "node --test .github/scripts/__tests__/*.test.js .github/scripts/__tests__/*.test.mjs",
6262
"test:provisioning": "node --test .github/scripts/provisioning/__tests__/*.test.js"
6363
},
6464
"keywords": [

0 commit comments

Comments
 (0)