Skip to content

Commit c3d6098

Browse files
authored
Merge branch 'asyncapi:master' into accessibility
2 parents 2550c87 + 2c75f98 commit c3d6098

File tree

103 files changed

+6008
-240
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+6008
-240
lines changed

.all-contributorsrc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,17 @@
448448
"contributions": [
449449
"doc"
450450
]
451+
},
452+
{
453+
"login": "gvensan",
454+
"name": "Giri Venkatesan",
455+
"avatar_url": "https://avatars.githubusercontent.com/u/4477169?v=4",
456+
"profile": "https://github.com/gvensan",
457+
"contributions": [
458+
"talk",
459+
"blog",
460+
"promotion"
461+
]
451462
}
452463
],
453464
"commitConvention": "angular",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Verify Member
2+
outputs:
3+
isTSCMember:
4+
description: 'Check whether the person is TSCMember or not'
5+
value: ${{steps.verify_member.outputs.isTSCMember}}
6+
inputs:
7+
authorName:
8+
description: 'Name of the commentor'
9+
required: true
10+
11+
runs:
12+
using: "composite"
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@v4
16+
17+
- name: Install the dependencies
18+
run: npm install js-yaml@4.1.0
19+
shell: bash
20+
21+
- name: Verify TSC Member
22+
id: verify_member
23+
uses: actions/github-script@v6
24+
with:
25+
script: |
26+
const yaml = require('js-yaml');
27+
const fs = require('fs');
28+
const commenterName = '${{ inputs.authorName }}';
29+
let isTSCMember = false;
30+
try {
31+
// Load YAML file
32+
const data = yaml.load(fs.readFileSync('MAINTAINERS.yaml', 'utf8'));
33+
34+
// Filter persons who are TSC members and whose GitHub username matches commenterName
35+
const isTscMember = data.find(person => {
36+
return (person.isTscMember === true || person.isTscMember === "true") && person.github === commenterName;
37+
});
38+
// Check if a TSC member was found
39+
if (isTscMember) {
40+
isTSCMember = true;
41+
}
42+
43+
core.setOutput('isTSCMember', isTSCMember);
44+
} catch (e) {
45+
console.log(e);
46+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github.api.cache.json
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Maintainers
2+
3+
The ["Update MAINTAINERS.yaml file"](../../workflows/update-maintainers.yaml) workflow, defined in the `community` repository performs a complete refresh by fetching all public repositories under AsyncAPI and their respective `CODEOWNERS` files.
4+
5+
## Workflow Execution
6+
7+
The "Update MAINTAINERS.yaml file" workflow is executed in the following scenarios:
8+
9+
1. **Weekly Schedule**: The workflow runs automatically every week. It is useful, e.g. when some repositories are archived, renamed, or when a GitHub user account is removed.
10+
2. **On Change**: When a `CODEOWNERS` file is changed in any repository under the AsyncAPI organization, the related repository triggers the workflow by emitting the `trigger-maintainers-update` event.
11+
3. **Manual Trigger**: Users can manually trigger the workflow as needed.
12+
13+
### Workflow Steps
14+
15+
1. **Load Cache**: Attempt to read previously cached data from `github.api.cache.json` to optimize API calls.
16+
2. **List All Repositories**: Retrieve a list of all public repositories under the AsyncAPI organization, skipping any repositories specified in the `IGNORED_REPOSITORIES` environment variable.
17+
3. **Fetch `CODEOWNERS` Files**: For each repository:
18+
- Detect the default branch (e.g., `main`, `master`, or a custom branch).
19+
- Check for `CODEOWNERS` files in all valid locations as specified in the [GitHub documentation](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location).
20+
4. **Process `CODEOWNERS` Files**:
21+
1. Extract GitHub usernames from each `CODEOWNERS` file, excluding emails, team names, and users specified by the `IGNORED_USERS` environment variable.
22+
2. Retrieve profile information for each unique GitHub username.
23+
3. Collect a fresh list of repositories currently owned by each GitHub user.
24+
5. **Refresh Maintainers List**: Iterate through the existing maintainers list:
25+
- Delete the entry if it:
26+
- Refers to a deleted GitHub account.
27+
- Was not found in any `CODEOWNERS` file across all repositories in the AsyncAPI organization.
28+
- Otherwise, update **only** the `repos` property.
29+
6. **Add New Maintainers**: Append any new maintainers not present in the previous list.
30+
7. **Changes Summary**: Provide details on why a maintainer was removed or changed directly on the GitHub Action [summary page](https://github.blog/2022-05-09-supercharging-github-actions-with-job-summaries/).
31+
8. **Save Cache**: Save retrieved data in `github.api.cache.json`.
32+
33+
## Job Details
34+
35+
- **Concurrency**: Ensures the workflow does not run multiple times concurrently to avoid conflicts.
36+
- **Wait for PRs to be Merged**: The workflow waits for pending pull requests to be merged before execution. If the merged pull request addresses all necessary fixes, it prevents unnecessary executions.
37+
38+
## Handling Conflicts
39+
40+
Since the job performs a full refresh each time, resolving conflicts is straightforward:
41+
42+
1. Close the pull request with conflicts.
43+
2. Navigate to the "Update MAINTAINERS.yaml file" workflow.
44+
3. Trigger it manually by clicking "Run workflow".
45+
46+
## Caching Mechanism
47+
48+
Each execution of this action performs a full refresh through the following API calls:
49+
50+
```
51+
ListRepos(AsyncAPI) # 1 call using GraphQL - not cached.
52+
for each Repo
53+
GetCodeownersFile(Repo) # N calls using REST API - all are cached. N refers to the number of public repositories under AsyncAPI.
54+
for each codeowner
55+
GetGitHubProfile(owner) # Y calls using REST API - all are cached. Y refers to unique GitHub users found across all CODEOWNERS files.
56+
```
57+
58+
To avoid hitting the GitHub API rate limits, [conditional requests](https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api?apiVersion=2022-11-28#use-conditional-requests-if-appropriate) are used via `if-modified-since`. The API responses are saved into a `github.api.cache.json` file, which is later uploaded as a GitHub action cache item.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const fs = require("fs");
2+
3+
module.exports = {
4+
fetchWithCache,
5+
saveCache,
6+
loadCache,
7+
printAPICallsStats,
8+
};
9+
10+
const CODEOWNERS_CACHE_PATH = "./.github/scripts/maintainers/github.api.cache.json";
11+
12+
let cacheEntries = {};
13+
14+
let numberOfFullFetches = 0;
15+
let numberOfCacheHits = 0;
16+
17+
function loadCache(core) {
18+
try {
19+
cacheEntries = JSON.parse(fs.readFileSync(CODEOWNERS_CACHE_PATH, "utf8"));
20+
} catch (error) {
21+
core.warning(`Cache was not restored: ${error}`);
22+
}
23+
}
24+
25+
function saveCache() {
26+
fs.writeFileSync(CODEOWNERS_CACHE_PATH, JSON.stringify(cacheEntries));
27+
}
28+
29+
async function fetchWithCache(cacheKey, fetchFn, core) {
30+
const cachedResp = cacheEntries[cacheKey];
31+
32+
try {
33+
const { data, headers } = await fetchFn({
34+
headers: {
35+
"if-modified-since": cachedResp?.lastModified ?? "",
36+
},
37+
});
38+
39+
cacheEntries[cacheKey] = {
40+
// last modified header is more reliable than etag while executing calls on GitHub Action
41+
lastModified: headers["last-modified"],
42+
data,
43+
};
44+
45+
numberOfFullFetches++;
46+
return data;
47+
} catch (error) {
48+
if (error.status === 304) {
49+
numberOfCacheHits++;
50+
core.debug(`Returning cached data for ${cacheKey}`);
51+
return cachedResp.data;
52+
}
53+
throw error;
54+
}
55+
}
56+
57+
function printAPICallsStats(core) {
58+
core.startGroup("API calls statistic");
59+
core.info(
60+
`Number of API calls count against rate limit: ${numberOfFullFetches}`,
61+
);
62+
core.info(`Number of cache hits: ${numberOfCacheHits}`);
63+
core.endGroup();
64+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
const { fetchWithCache } = require("./cache");
2+
3+
module.exports = { getGitHubProfile, getAllCodeownersFiles, getRepositories };
4+
5+
async function getRepositories(github, owner, ignoredRepos, core) {
6+
core.startGroup(
7+
`Getting list of all public, non-archived repositories owned by ${owner}`,
8+
);
9+
10+
const query = `
11+
query repos($cursor: String, $owner: String!) {
12+
organization(login: $owner) {
13+
repositories(first: 100 after: $cursor visibility: PUBLIC isArchived: false orderBy: {field: CREATED_AT, direction: ASC} ) {
14+
nodes {
15+
name
16+
}
17+
pageInfo {
18+
hasNextPage
19+
endCursor
20+
}
21+
}
22+
}
23+
}`;
24+
25+
const repos = [];
26+
let cursor = null;
27+
28+
do {
29+
const result = await github.graphql(query, { owner, cursor });
30+
const { nodes, pageInfo } = result.organization.repositories;
31+
repos.push(...nodes);
32+
33+
cursor = pageInfo.hasNextPage ? pageInfo.endCursor : null;
34+
} while (cursor);
35+
36+
core.debug(`List of repositories for ${owner}:`);
37+
core.debug(JSON.stringify(repos, null, 2));
38+
core.endGroup();
39+
40+
return repos.filter((repo) => !ignoredRepos.includes(repo.name));
41+
}
42+
43+
async function getGitHubProfile(github, login, core) {
44+
try {
45+
const profile = await fetchWithCache(
46+
`profile:${login}`,
47+
async ({ headers }) => {
48+
return github.rest.users.getByUsername({
49+
username: login,
50+
headers,
51+
});
52+
},
53+
core,
54+
);
55+
return removeNulls({
56+
name: profile.name ?? login,
57+
github: login,
58+
twitter: profile.twitter_username,
59+
availableForHire: profile.hireable,
60+
isTscMember: false,
61+
repos: [],
62+
githubID: profile.id,
63+
});
64+
} catch (error) {
65+
if (error.status === 404) {
66+
return null;
67+
}
68+
throw error;
69+
}
70+
}
71+
72+
// Checks for all valid locations according to:
73+
// https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location
74+
//
75+
// Detect the repository default branch automatically.
76+
async function getCodeownersFile(github, owner, repo, core) {
77+
const paths = ["CODEOWNERS", "docs/CODEOWNERS", ".github/CODEOWNERS"];
78+
79+
for (const path of paths) {
80+
try {
81+
core.debug(
82+
`[repo: ${owner}/${repo}]: Fetching CODEOWNERS file at ${path}`,
83+
);
84+
return await fetchWithCache(
85+
`owners:${owner}/${repo}`,
86+
async ({ headers }) => {
87+
return github.rest.repos.getContent({
88+
owner,
89+
repo,
90+
path,
91+
headers: {
92+
Accept: "application/vnd.github.raw+json",
93+
...headers,
94+
},
95+
});
96+
},
97+
core,
98+
);
99+
} catch (error) {
100+
core.warning(
101+
`[repo: ${owner}/${repo}]: Failed to fetch CODEOWNERS file at ${path}: ${error.message}`,
102+
);
103+
}
104+
}
105+
106+
core.error(
107+
`[repo: ${owner}/${repo}]: CODEOWNERS file not found in any of the expected locations.`,
108+
);
109+
return null;
110+
}
111+
112+
async function getAllCodeownersFiles(github, owner, repos, core) {
113+
core.startGroup(`Fetching CODEOWNERS files for ${repos.length} repositories`);
114+
const files = [];
115+
for (const repo of repos) {
116+
const data = await getCodeownersFile(github, owner, repo.name, core);
117+
if (!data) {
118+
continue;
119+
}
120+
files.push({
121+
repo: repo.name,
122+
content: data,
123+
});
124+
}
125+
core.endGroup();
126+
return files;
127+
}
128+
129+
function removeNulls(obj) {
130+
return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null));
131+
}

0 commit comments

Comments
 (0)