Skip to content

Commit b140fb7

Browse files
Merge pull request #26 from adamhough/adamhough-patch-1
Create 000-adr-process.md to record the zeroth ADR.
2 parents 95ff871 + 97c2835 commit b140fb7

File tree

7 files changed

+399
-3
lines changed

7 files changed

+399
-3
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* Compute team approval majority and set step outputs */
2+
module.exports = async function ({ github, context, core }) {
3+
const owner = process.env.ORG;
4+
const repo = context.repo.repo;
5+
const prNum = context.issue.number;
6+
7+
const members = await github.paginate(
8+
github.rest.teams.listMembersInOrg,
9+
{ org: owner, team_slug: process.env.TEAM_SLUG, per_page: 100 }
10+
);
11+
12+
const teamLogins = members.map(m => m.login).sort((a, b) => a.localeCompare(b));
13+
const team = new Set(teamLogins.map(s => s.toLowerCase()));
14+
const teamSize = team.size;
15+
const needed = Math.floor(teamSize / 2) + 1;
16+
17+
const reviews = await github.paginate(
18+
github.rest.pulls.listReviews,
19+
{ owner, repo, pull_number: prNum, per_page: 100 }
20+
);
21+
22+
const latestByUser = new Map();
23+
for (const r of reviews) {
24+
if (!r.user) continue;
25+
latestByUser.set(r.user.login.toLowerCase(), r.state);
26+
}
27+
28+
const approvers = [];
29+
for (const [login, state] of latestByUser.entries()) {
30+
if (state === 'APPROVED' && team.has(login)) approvers.push(login);
31+
}
32+
approvers.sort((a, b) => a.localeCompare(b));
33+
34+
const have = approvers.length;
35+
const remaining = teamLogins.filter(u => !approvers.includes(u.toLowerCase()));
36+
const majority = have >= needed;
37+
38+
core.setOutput('have', String(have));
39+
core.setOutput('needed', String(needed));
40+
core.setOutput('teamSize', String(teamSize));
41+
core.setOutput('approvers_json', JSON.stringify(approvers));
42+
core.setOutput('remaining_json', JSON.stringify(remaining));
43+
core.setOutput('majority', String(majority));
44+
};

.github/scripts/merge-if-ready.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/* Merge the PR if checks are successful and majority has been reached */
2+
module.exports = async function ({ github, context, core }) {
3+
const owner = context.repo.owner;
4+
const repo = context.repo.repo;
5+
const prNum = context.issue.number;
6+
7+
const pr = (await github.rest.pulls.get({ owner, repo, pull_number: prNum })).data;
8+
const status = await github.rest.repos.getCombinedStatusForRef({
9+
owner, repo, ref: pr.head.sha
10+
});
11+
12+
if (status.data.state !== 'success') {
13+
core.info(`Checks not successful yet (combined status = ${status.data.state}).`);
14+
return;
15+
}
16+
17+
await github.rest.pulls.merge({
18+
owner,
19+
repo,
20+
pull_number: prNum,
21+
merge_method: process.env.MERGE_METHOD
22+
});
23+
core.info('Merged by team majority ✔️');
24+
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* Post or update a sticky PR comment with approval status */
2+
module.exports = async function ({ github, context, core }) {
3+
const {
4+
MARK,
5+
HAVE,
6+
NEEDED,
7+
TEAM_SIZE,
8+
APPROVERS_JSON,
9+
REMAINING_JSON,
10+
TEAM_SLUG
11+
} = process.env;
12+
13+
let approvers = [];
14+
let remaining = [];
15+
16+
try {
17+
if (APPROVERS_JSON) approvers = JSON.parse(APPROVERS_JSON);
18+
if (!Array.isArray(approvers)) { approvers = []; core.warning('Approvers not an array after parse.'); }
19+
} catch (e) {
20+
core.warning(`Could not parse APPROVERS_JSON='${APPROVERS_JSON}': ${e.message}`);
21+
}
22+
try {
23+
if (REMAINING_JSON) remaining = JSON.parse(REMAINING_JSON);
24+
if (!Array.isArray(remaining)) { remaining = []; core.warning('Remaining not an array after parse.'); }
25+
} catch (e) {
26+
core.warning(`Could not parse REMAINING_JSON='${REMAINING_JSON}': ${e.message}`);
27+
}
28+
29+
const have = Number(HAVE);
30+
const needed = Number(NEEDED);
31+
const teamSize = Number(TEAM_SIZE);
32+
33+
const fmtList = arr => (arr.length ? arr.map(u => `@${u}`).join(', ') : '_none_');
34+
const statusLine = have >= needed
35+
? `✅ **Majority reached:** ${have}/${needed} approvals from \`${TEAM_SLUG}\`.`
36+
: `⏳ **Approvals:** ${have}/${needed} from \`${TEAM_SLUG}\`. Need **${needed - have}** more.`;
37+
38+
const body = `${MARK}
39+
${statusLine}
40+
41+
**Approved by:** ${fmtList(approvers)}
42+
**Still needed from:** ${fmtList(remaining)}
43+
44+
<sub>Team size considered: ${teamSize}. This comment auto-updates as reviews change.</sub>`;
45+
46+
const owner = context.repo.owner;
47+
const repo = context.repo.repo;
48+
const prNum = context.issue.number;
49+
50+
const comments = await github.paginate(
51+
github.rest.issues.listComments,
52+
{ owner, repo, issue_number: prNum, per_page: 100 }
53+
);
54+
const existing = comments.find(c => c.body && c.body.includes(MARK));
55+
56+
if (existing) {
57+
await github.rest.issues.updateComment({
58+
owner, repo, comment_id: existing.id, body
59+
});
60+
core.info('Updated existing majority status comment.');
61+
} else {
62+
await github.rest.issues.createComment({
63+
owner, repo, issue_number: prNum, body
64+
});
65+
core.info('Created new majority status comment.');
66+
}
67+
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Merge on Team Majority
2+
3+
on:
4+
pull_request_review:
5+
types: [submitted, edited, dismissed]
6+
paths:
7+
- 'adrs/**'
8+
pull_request:
9+
types: [synchronize, reopened, labeled]
10+
paths:
11+
- 'adrs/**'
12+
workflow_dispatch:
13+
inputs:
14+
pr_number:
15+
description: 'Pull Request Number'
16+
required: false
17+
default: ''
18+
sha:
19+
description: 'Commit SHA'
20+
required: false
21+
default: ''
22+
23+
permissions:
24+
contents: write
25+
pull-requests: write
26+
27+
env:
28+
ORG: openchami
29+
TEAM_SLUG: tsc
30+
MERGE_METHOD: squash
31+
32+
jobs:
33+
team-majority:
34+
runs-on: ubuntu-latest
35+
steps:
36+
- name: Check out repository
37+
uses: actions/checkout@v4
38+
with:
39+
fetch-depth: 0
40+
41+
- name: Compute team approval status
42+
id: compute
43+
uses: actions/github-script@v7
44+
with:
45+
github-token: ${{ secrets.ORG_READ_TOKEN }}
46+
script: |
47+
const run = require('./.github/scripts/compute-team-majority.js');
48+
await run({ github, context, core });
49+
50+
# - name: Debug outputs
51+
# run: |
52+
# echo "have=${{ steps.compute.outputs.have }}"
53+
# echo "needed=${{ steps.compute.outputs.needed }}"
54+
# echo "teamSize=${{ steps.compute.outputs.teamSize }}"
55+
# echo "approvers=${{ steps.compute.outputs.approvers_json }}"
56+
# echo "remaining=${{ steps.compute.outputs.remaining_json }}"
57+
# echo "majority=${{ steps.compute.outputs.majority }}"
58+
59+
- name: Update sticky status comment
60+
id: comment
61+
uses: actions/github-script@v7
62+
env:
63+
MARK: '<!-- team-majority-status -->'
64+
HAVE: ${{ steps.compute.outputs.have }}
65+
NEEDED: ${{ steps.compute.outputs.needed }}
66+
TEAM_SIZE: ${{ steps.compute.outputs.teamSize }}
67+
APPROVERS_JSON: ${{ steps.compute.outputs.approvers_json }}
68+
REMAINING_JSON: ${{ steps.compute.outputs.remaining_json }}
69+
TEAM_SLUG: ${{ env.TEAM_SLUG }}
70+
with:
71+
github-token: ${{ github.token }}
72+
script: |
73+
const run = require('./.github/scripts/update-majority-comment.js');
74+
await run({ github, context, core });
75+
76+
- name: Merge if ready
77+
if: ${{ steps.compute.outputs.majority == 'true' }}
78+
uses: actions/github-script@v7
79+
with:
80+
github-token: ${{ github.token }}
81+
script: |
82+
const run = require('./.github/scripts/merge-if-ready.js');
83+
await run({ github, context, core });

.github/workflows/reindex.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Regenerate ADR Index
2+
3+
on:
4+
push:
5+
paths:
6+
- 'adr/**'
7+
8+
jobs:
9+
regenerate-index:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout repository
13+
uses: actions/checkout@v4
14+
15+
- name: Regenerate ADR index
16+
uses: alexlovelltroy/adrctl-action@v1
17+
with:
18+
command: index
19+
directory: adr
20+
21+
- name: Commit and push changes
22+
run: |
23+
git config --local user.name "github-actions[bot]"
24+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
25+
if [[ -n $(git status --porcelain adr/index.md) ]]; then
26+
git add adr/index.md
27+
git commit -m "Regenerate ADR index.md [skip ci]"
28+
git push
29+
fi

adr/000-adr-process.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
id: 000
3+
title: "Establish an Architecture Decision Record (ADR) Process for Documenting Key Decisions"
4+
status: "Accepted"
5+
date: "2025-07-03"
6+
---
7+
8+
# ADR [000]: Establish an Architecture Decision Record (ADR) Process for Documenting Key Decisions
9+
10+
**Date Proposed:** 2025-07-03
11+
**Status:** Accepted
12+
13+
**Participants**
14+
- [@alexlovelltroy](https://github.com/alexlovelltroy)
15+
- [@cjh1](https://github.com/cjh1)
16+
- [@haroldlongley](https://github.com/haroldlongley)
17+
- [@j0hnL](https://github.com/j0hnL)
18+
- [@mdklein](https://github.com/mdklein)
19+
- [@rainest](https://github.com/rainest)
20+
- [@trcotton](https://github.com/trcotton)
21+
- [@adamhough](https://github.com/adamhough)
22+
23+
---
24+
25+
**Context:**
26+
27+
Implement an Architecture Decision Record (ADR) process to systematically document and maintain key architectural and technical decisions.
28+
Benefits:
29+
30+
- Transparency: Provides a clear history of decisions and the reasoning behind them.
31+
- Consistency: Ensures decisions are documented in a uniform manner.
32+
- Onboarding: Helps new contributors understand past decisions and the project's direction.
33+
- Reference: Serves as a centralized repository for decision-making, reducing redundancy and confusion.
34+
35+
---
36+
37+
**Decision:**
38+
39+
## Summary of Decisions from 2025 OpenCHAMI Summit
40+
41+
### Timing Constraints for ADR Decisions
42+
- Proposals should be decided within **6 weeks** of submission.
43+
- Ensures at least one monthly **TSC meeting** can used to review the proposal if needed.
44+
45+
### Revised Acceptance Criteria
46+
- Move away from approval by just **2 TSC members**.
47+
- Require a **majority vote** from the TSC.
48+
49+
### ADR Template Improvements
50+
- Add a section for **“Other Options Considered”** to document alternative approaches considered.
51+
- Include **“Non-goals”** and **points of contention** to clarify scope and disagreements.
52+
53+
### Dashboard for ADR Requests
54+
- Desire for a **visual board** showing ADRs pending review or decision.
55+
- Should include:
56+
- Status updates
57+
- Expected decision timelines
58+
- Notifications when new ADRs are added
59+
60+
### Handling Low-Frequency TSC Meetings
61+
- Monthly meetings are too infrequent for timely decisions.
62+
- Suggest handling ADRs **asynchronously**, similar to [OpenCHAMI/roadmap#95](https://github.com/OpenCHAMI/roadmap/issues/95).
63+
- Maintain a board of ADRs expected to be discussed.
64+
- Announce new ADRs when added.
65+
- Require a **minimum 6-week lead time** before decisions.
66+
67+
### ADR Number Reservation
68+
- Add instructions in the ADR README:
69+
- How to **reserve a number** by creating a branch with the number included.
70+
71+
---
72+
73+
**Other Options Considered:**
74+
No other options considers as this is required.
75+
76+
---
77+
78+
**Consequences:**
79+
## If No ADR process were defined:
80+
81+
1. **Lost Rationale**
82+
No record of *why* decisions were made → confusion and repeated debates.
83+
84+
2. **Knowledge Drain**
85+
Decisions leave with people → hard to maintain continuity.
86+
87+
3. **Repetition & Waste**
88+
Teams revisit the same issues → slows progress and wastes time.
89+
90+
4. **Poor Accountability**
91+
No traceability → harder to learn from mistakes or improve.
92+
93+
5. **Misalignment**
94+
Stakeholders may misunderstand decisions → leads to friction and rework.
95+
96+
6. **Architectural Drift**
97+
Inconsistent decisions over time → increases technical debt.
98+
99+
---
100+
101+
**Non-Goals:**
102+
N/A
103+
104+
---
105+
106+
**Points of Contention:**
107+
N/A
108+
109+
---
110+
111+
**Notes:**
112+
113+
- [ ] Create ADR instructions in this repository
114+
- [ ] Establish automation for votes and timeouts
115+
- [ ] Establish dashboard(s) for use at TSC meetings
116+
117+
---
118+
119+
**References:**
120+
- [OpenCHAMI roadmap issue #98](https://github.com/OpenCHAMI/roadmap/issues/98)
121+
- [AWS ADR Process](https://docs.aws.amazon.com/prescriptive-guidance/latest/architectural-decision-records/adr-process.html)
122+
- [GitHub adr organization](https://adr.github.io/)
123+
- [Timing Architectural Decisions (ADs)](https://ozimmer.ch/assets/presos/ZIO-ITARCKeynoteTADv101p.pdf)
124+
- [Alex Lovell-Troy's `adrctl`](https://github.com/alexlovelltroy/adrctl)
125+
- ADR Template in this repository [template.md](/adr/template.md)

0 commit comments

Comments
 (0)