Skip to content

Commit

Permalink
ci: refresh MAINTAINERS.yaml for each CODEOWNERS file change
Browse files Browse the repository at this point in the history
  • Loading branch information
mszostok committed Jul 19, 2024
1 parent e27f9f1 commit 7352592
Show file tree
Hide file tree
Showing 12 changed files with 4,680 additions and 1 deletion.
20 changes: 19 additions & 1 deletion .github/workflows/global-replicator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ jobs:
with:
github_token: ${{ secrets.GH_TOKEN }}
patterns_to_include: .github/workflows/scripts,.github/workflows/automerge-for-humans-add-ready-to-merge-or-do-not-merge-label.yml,.github/workflows/add-good-first-issue-labels.yml,.github/workflows/automerge-for-humans-merging.yml,.github/workflows/automerge-for-humans-remove-ready-to-merge-label-on-edit.yml,.github/workflows/automerge-orphans.yml,.github/workflows/automerge.yml,.github/workflows/autoupdate.yml,.github/workflows/help-command.yml,.github/workflows/issues-prs-notifications.yml,.github/workflows/lint-pr-title.yml,.github/workflows/notify-tsc-members-mention.yml,.github/workflows/stale-issues-prs.yml,.github/workflows/welcome-first-time-contrib.yml,.github/workflows/release-announcements.yml,.github/workflows/bounty-program-commands.yml,.github/workflows/please-take-a-look-command.yml,.github/workflows/update-pr.yml
patterns_to_ignore: .github/workflows/scripts/maintainers
committer_username: asyncapi-bot
committer_email: [email protected]
commit_message: "ci: update of files from global .github repo"
Expand Down Expand Up @@ -215,4 +216,21 @@ jobs:
committer_username: asyncapi-bot
committer_email: [email protected]
commit_message: "ci: update .prettierignore from global .github repo"
bot_branch_name: bot/update-files-from-global-repo
bot_branch_name: bot/update-files-from-global-repo

replicate_refresh_maintainers_workflow:
if: startsWith(github.repository, 'asyncapi/')
name: Replicate refresh-maintainers.yml workflow in the required repositories
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Replicating file
uses: derberg/manage-files-in-multiple-repositories@beecbe897cf5ed7f3de5a791a3f2d70102fe7c25
with:
github_token: ${{ secrets.GH_TOKEN }}
patterns_to_include: .github/workflows/refresh-maintainers.yaml,.github/workflows/scripts/maintainers
committer_username: asyncapi-bot
committer_email: [email protected]
commit_message: "ci: update refresh-maintainers.yml workflow from global .github repo"
bot_branch_name: bot/update-files-from-global-repo
136 changes: 136 additions & 0 deletions .github/workflows/refresh-maintainers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: Refresh MAINTAINERS.yaml file

on:
push:
branches:
- main
paths:
- 'CODEOWNERS'
- '.github/workflows/scripts/maintainers/**'
- '.github/workflows/refresh-maintainers.yaml'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

env:
IGNORED_REPOSITORIES: "github-action-for-cli, shape-up-process"
IGNORED_USERS: "asyncapi-bot-eve"

GIT_USER: asyncapi-bot
GIT_EMAIL: [email protected]

BRANCH_NAME: "bot/update-maintainers-${{ github.run_id }}"
PR_TITLE: "docs(maintainers): Update MAINTAINERS.yaml file with the latest CODEOWNERS changes"

jobs:
update-maintainers:
permissions:
contents: write
pull-requests: write

name: Update MAINTAINERS.yaml based on CODEOWNERS files in all organization repositories
runs-on: ubuntu-latest

steps:
- name: Wait for active pull requests to be merged
env:
GH_TOKEN: ${{ github.token }}
TIMEOUT: 300 # Timeout in seconds
INTERVAL: 5 # Check interval in seconds
run: |
check_active_prs() {
ACTIVE_PULL_REQUESTS=$(gh -R $GITHUB_REPOSITORY pr list --search "is:pr ${PR_TITLE} in:title" --json id)
if [ "$ACTIVE_PULL_REQUESTS" == "[]" ]; then
return 1 # No active PRs
else
return 0 # Active PRs found
fi
}
# Loop with timeout
elapsed_time=0
while [ $elapsed_time -lt $TIMEOUT ]; do
if check_active_prs; then
echo "There is an active pull request. Waiting for it to be merged..."
else
echo "There is no active pull request. Proceeding with refreshing MAINTAINERS file."
exit 0
fi
sleep $INTERVAL
elapsed_time=$((elapsed_time + INTERVAL))
done
echo "Timeout reached. Proceeding with refreshing MAINTAINERS file with active pull request(s) present. It may result in merge conflict."
exit 0
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: 'npm'
node-version-file: './.github/workflows/scripts/maintainers/package.json'
cache-dependency-path: './.github/workflows/scripts/maintainers/package-lock.json'

- name: Install dependencies
working-directory: ./.github/workflows/scripts/maintainers
run: npm install

- name: Restore cached GitHub API calls
uses: actions/cache/restore@v4
id: restore-cache
with:
path: ./.github/workflows/scripts/maintainers/github.api.cache.json
key: github-api-cache
restore-keys: |
github-api-cache-
- name: Run script updating MAINTAINERS.yaml
working-directory: ./.github/workflows/scripts/maintainers
run: npm run start
env:
GH_TOKEN: ${{ github.token }}
MAINTAINERS_FILE_PATH: "${{ github.workspace }}/MAINTAINERS.yaml"

- name: Save cached GitHub API calls
uses: actions/cache/save@v4
with:
path: ./.github/workflows/scripts/maintainers/github.api.cache.json
# re-evaluate the key, so we update cache when file changes
key: github-api-cache-${{ hashfiles('./.github/workflows/scripts/maintainers/github.api.cache.json') }}

- name: Detect git changes
id: check_changes
run: |
if [[ $(git diff --stat) != '' ]]; then
echo "CHANGES=true" >> $GITHUB_OUTPUT
else
echo '✔ No changes detected. Have a nice day :-)'
fi
- name: Create PR with latest changes
if: steps.check_changes.outputs.CHANGES == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
git config --global user.name "${{ env.GIT_USER }}"
git config --global user.email ${{ env.GIT_EMAIL }}
git checkout -b "${{ env.BRANCH_NAME }}"
git add -A
git commit -m "${{ env.PR_TITLE }}"
git push origin "${{ env.BRANCH_NAME }}" -f
gh pr create --title "${{ env.PR_TITLE }}" --body "Updated MAINTAINERS.yaml based on CODEOWNERS files in all organization repositories" --head ${{ env.BRANCH_NAME }}
- name: Report workflow run status to Slack
uses: rtCamp/action-slack-notify@4e5fb42d249be6a45a298f3c9543b111b02f7907 # https://github.com/rtCamp/action-slack-notify/releases/tag/v2.3.0
if: failure()
env:
SLACK_WEBHOOK: ${{secrets.SLACK_CI_FAIL_NOTIFY}}
SLACK_TITLE: 🚨 Refresh MAINTAINERS.yaml file Workflow failed 🚨
SLACK_MESSAGE: Failed to refresh MAINTAINERS.yaml file.
MSG_MINIMAL: true
37 changes: 37 additions & 0 deletions .github/workflows/scripts/maintainers/.eslintrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
env:
es6: true
node: true
commonjs: true
globals:
Atomics: readonly
SharedArrayBuffer: readonly

ignorePatterns:
- "!.*"
- "**/node_modules/.*"
- "**/dist/.*"
- "**/coverage/.*"
- "*.json"

parserOptions:
ecmaVersion: 2023
sourceType: module
requireConfigFile: false

extends:
- eslint:recommended
- plugin:github/recommended

rules:
{
"camelcase": "off",
"eslint-comments/no-use": "off",
"eslint-comments/no-unused-disable": "off",
"i18n-text/no-en": "off",
"import/no-commonjs": "off",
"import/no-namespace": "off",
"no-console": "off",
"no-unused-vars": "off",
"prettier/prettier": "error",
"semi": "off",
}
2 changes: 2 additions & 0 deletions .github/workflows/scripts/maintainers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/node_modules
github.api.cache.json
2 changes: 2 additions & 0 deletions .github/workflows/scripts/maintainers/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
node_modules/
57 changes: 57 additions & 0 deletions .github/workflows/scripts/maintainers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Maintainers Refresher

The ["Refresh MAINTAINERS.yaml file"](../../refresh-maintainers.yaml) workflow, defined in the `community` repository performs a complete refresh by fetching all public repositories under AsyncAPI and their respective `CODEOWNERS` files.

### Workflow Steps

1. **Load Cache**: Attempt to read previously cached data from `github.api.cache.json` to optimize API calls.
2. **List All Repositories**: Retrieve a list of all public repositories under the AsyncAPI organization, skipping any repositories listed in the `IGNORED_REPOSITORIES` environment variable.
3. **Fetch `CODEOWNERS` Files**: For each repository:
- Detect the default branch (e.g., `main`, `master`, or a custom branch).
- 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).
4. **Process `CODEOWNERS` Files**:
1. Extract GitHub usernames from each `CODEOWNERS` file, excluding emails, team names, and users listed by the `IGNORED_USERS` environment variable.
2. Retrieve profile information for each unique GitHub username.
3. Collect a fresh list of repositories currently owned by each GitHub user.
5. **Refresh Maintainers List**: Iterate through the existing maintainers list:
- Delete the entry if it:
- Refers to a deleted GitHub account.
- Was not found in any `CODEOWNERS` file across all repositories in the AsyncAPI organization.
- Otherwise, update only the `repos` property.
6. **Add New Maintainers**: Append any new maintainers not present in the previous list.
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/).
8. **Save Cache**: Save retrieved data in `github.api.cache.json`.

## Workflow Execution

The "Refresh MAINTAINERS.yaml file" workflow is executed in the following scenarios:
1. **Weekly Schedule**: The workflow runs automatically every week.
2. **On-Demand**: When a `CODEOWNERS` file is changed in any repository under the AsyncAPI organization, the source repository triggers the workflow by emitting an event.
3. **Manual Trigger**: Users can manually trigger the workflow as needed.

## Job Details

- **Concurrency**: Ensures the workflow does not run multiple times concurrently to avoid conflicts.
- **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.

## Handling Conflicts

Since the job performs a full refresh each time, resolving conflicts is straightforward:

1. Close the pull request with conflicts.
2. Navigate to the "Refresh MAINTAINERS.yaml file" workflow.
3. Trigger it manually by clicking "Run workflow".

## Caching Mechanism

Each execution of this action performs a full refresh through the following API calls:

```
ListRepos(AsyncAPI) # 1 call using GraphQL - not cached.
for each Repo
GetCodeownersFile(Repo) # N calls using REST API - all are cached. N refers to the number of public repositories under AsyncAPI.
for each codeowner
GetGitHubProfile(owner) # Y calls using REST API - all are cached. Y refers to unique GitHub users found across all CODEOWNERS files.
```

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.
65 changes: 65 additions & 0 deletions .github/workflows/scripts/maintainers/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const fs = require("fs");
const core = require("@actions/core");

module.exports = {
fetchWithCache,
saveCache,
loadCache,
printAPICallsStats,
};

const CODEOWNERS_CACHE_PATH = "github.api.cache.json";

let cacheEntries = {};

let numberOfFullFetches = 0;
let numberOfCacheHits = 0;

async function loadCache() {
try {
cacheEntries = JSON.parse(fs.readFileSync(CODEOWNERS_CACHE_PATH, "utf8"));
} catch (error) {
core.warning(`Cache was not restored: ${error}`);
}
}

async function saveCache() {
fs.writeFileSync(CODEOWNERS_CACHE_PATH, JSON.stringify(cacheEntries));
}

async function fetchWithCache(cacheKey, fetchFn) {
const cachedResp = cacheEntries[cacheKey];

try {
const { data, headers } = await fetchFn({
headers: {
"if-modified-since": cachedResp?.lastModified ?? "",
},
});

cacheEntries[cacheKey] = {
// last modified header is more reliable than etag while executing calls on GitHub Action
lastModified: headers["last-modified"],
data,
};

numberOfFullFetches++;
return data;
} catch (error) {
if (error.status === 304) {
numberOfCacheHits++;
core.debug(`Returning cached data for ${cacheKey}`);
return cachedResp.data;
}
throw error;
}
}

function printAPICallsStats() {
core.startGroup("API calls statistic");
core.info(
`Number of API calls count against rate limit: ${numberOfFullFetches}`,
);
core.info(`Number of cache hits: ${numberOfCacheHits}`);
core.endGroup();
}
Loading

0 comments on commit 7352592

Please sign in to comment.