diff --git a/.github/scripts/maintainers/.gitignore b/.github/scripts/maintainers/.gitignore new file mode 100644 index 000000000..60923f546 --- /dev/null +++ b/.github/scripts/maintainers/.gitignore @@ -0,0 +1 @@ +github.api.cache.json diff --git a/.github/scripts/maintainers/README.md b/.github/scripts/maintainers/README.md new file mode 100644 index 000000000..6d82e01e4 --- /dev/null +++ b/.github/scripts/maintainers/README.md @@ -0,0 +1,58 @@ +# Maintainers + +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. + +## Workflow Execution + +The "Update MAINTAINERS.yaml file" workflow is executed in the following scenarios: + +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. +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. +3. **Manual Trigger**: Users can manually trigger the workflow as needed. + +### 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 specified 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 specified 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`. + +## 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 "Update 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. diff --git a/.github/scripts/maintainers/cache.js b/.github/scripts/maintainers/cache.js new file mode 100644 index 000000000..0a52b4b7e --- /dev/null +++ b/.github/scripts/maintainers/cache.js @@ -0,0 +1,64 @@ +const fs = require("fs"); + +module.exports = { + fetchWithCache, + saveCache, + loadCache, + printAPICallsStats, +}; + +const CODEOWNERS_CACHE_PATH = "./.github/scripts/maintainers/github.api.cache.json"; + +let cacheEntries = {}; + +let numberOfFullFetches = 0; +let numberOfCacheHits = 0; + +function loadCache(core) { + try { + cacheEntries = JSON.parse(fs.readFileSync(CODEOWNERS_CACHE_PATH, "utf8")); + } catch (error) { + core.warning(`Cache was not restored: ${error}`); + } +} + +function saveCache() { + fs.writeFileSync(CODEOWNERS_CACHE_PATH, JSON.stringify(cacheEntries)); +} + +async function fetchWithCache(cacheKey, fetchFn, core) { + 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) { + 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(); +} diff --git a/.github/scripts/maintainers/gh_calls.js b/.github/scripts/maintainers/gh_calls.js new file mode 100644 index 000000000..f10b5c2eb --- /dev/null +++ b/.github/scripts/maintainers/gh_calls.js @@ -0,0 +1,131 @@ +const { fetchWithCache } = require("./cache"); + +module.exports = { getGitHubProfile, getAllCodeownersFiles, getRepositories }; + +async function getRepositories(github, owner, ignoredRepos, core) { + core.startGroup( + `Getting list of all public, non-archived repositories owned by ${owner}`, + ); + + const query = ` + query repos($cursor: String, $owner: String!) { + organization(login: $owner) { + repositories(first: 100 after: $cursor visibility: PUBLIC isArchived: false orderBy: {field: CREATED_AT, direction: ASC} ) { + nodes { + name + } + pageInfo { + hasNextPage + endCursor + } + } + } + }`; + + const repos = []; + let cursor = null; + + do { + const result = await github.graphql(query, { owner, cursor }); + const { nodes, pageInfo } = result.organization.repositories; + repos.push(...nodes); + + cursor = pageInfo.hasNextPage ? pageInfo.endCursor : null; + } while (cursor); + + core.debug(`List of repositories for ${owner}:`); + core.debug(JSON.stringify(repos, null, 2)); + core.endGroup(); + + return repos.filter((repo) => !ignoredRepos.includes(repo.name)); +} + +async function getGitHubProfile(github, login, core) { + try { + const profile = await fetchWithCache( + `profile:${login}`, + async ({ headers }) => { + return github.rest.users.getByUsername({ + username: login, + headers, + }); + }, + core, + ); + return removeNulls({ + name: profile.name ?? login, + github: login, + twitter: profile.twitter_username, + availableForHire: profile.hireable, + isTscMember: false, + repos: [], + githubID: profile.id, + }); + } catch (error) { + if (error.status === 404) { + return null; + } + throw error; + } +} + +// Checks for all valid locations according to: +// https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location +// +// Detect the repository default branch automatically. +async function getCodeownersFile(github, owner, repo, core) { + const paths = ["CODEOWNERS", "docs/CODEOWNERS", ".github/CODEOWNERS"]; + + for (const path of paths) { + try { + core.debug( + `[repo: ${owner}/${repo}]: Fetching CODEOWNERS file at ${path}`, + ); + return await fetchWithCache( + `owners:${owner}/${repo}`, + async ({ headers }) => { + return github.rest.repos.getContent({ + owner, + repo, + path, + headers: { + Accept: "application/vnd.github.raw+json", + ...headers, + }, + }); + }, + core, + ); + } catch (error) { + core.warning( + `[repo: ${owner}/${repo}]: Failed to fetch CODEOWNERS file at ${path}: ${error.message}`, + ); + } + } + + core.error( + `[repo: ${owner}/${repo}]: CODEOWNERS file not found in any of the expected locations.`, + ); + return null; +} + +async function getAllCodeownersFiles(github, owner, repos, core) { + core.startGroup(`Fetching CODEOWNERS files for ${repos.length} repositories`); + const files = []; + for (const repo of repos) { + const data = await getCodeownersFile(github, owner, repo.name, core); + if (!data) { + continue; + } + files.push({ + repo: repo.name, + content: data, + }); + } + core.endGroup(); + return files; +} + +function removeNulls(obj) { + return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null)); +} diff --git a/.github/scripts/maintainers/index.js b/.github/scripts/maintainers/index.js new file mode 100644 index 000000000..be32d8da5 --- /dev/null +++ b/.github/scripts/maintainers/index.js @@ -0,0 +1,190 @@ +const yaml = require("js-yaml"); +const fs = require("fs"); +const { saveCache, loadCache, printAPICallsStats } = require("./cache"); +const { summarizeChanges } = require("./summary"); +const { + getAllCodeownersFiles, + getGitHubProfile, + getRepositories, +} = require("./gh_calls"); + +module.exports = async ({ github, context, core }) => { + try { + await run(github, context, core); + } catch (error) { + console.log(error); + core.setFailed(`An error occurred: ${error}`); + } +}; + +const config = { + ghToken: process.env.GH_TOKEN, + ignoredRepos: getCommaSeparatedInputList(process.env.IGNORED_REPOSITORIES), + ignoredUsers: getCommaSeparatedInputList(process.env.IGNORED_USERS), + maintainersFilePath: process.env.MAINTAINERS_FILE_PATH, +}; + +function getCommaSeparatedInputList(list) { + return ( + list + ?.split(",") + .map((item) => item.trim()) + .filter((item) => item !== "") ?? [] + ); +} + +function splitByWhitespace(line) { + return line.trim().split(/\s+/); +} + +function extractGitHubUsernames(codeownersContent, core) { + if (!codeownersContent) return []; + + const uniqueOwners = new Set(); + + for (const line of codeownersContent.split("\n")) { + // split by '#' to process comments separately + const [ownersLine, comment = ""] = line.split("#"); + + // 1. Check AsyncAPI custom owners + const triagers = comment.split(/docTriagers:|codeTriagers:/)[1] + if (triagers) { + const owners = splitByWhitespace(triagers) + owners.forEach(owner => uniqueOwners.add(owner)) + } + + // 2. Check GitHub native codeowners + const owners = splitByWhitespace(ownersLine); + + // the 1st element is the file location, we don't need it, so we start with 2nd item + for (const owner of owners.slice(1)) { + if (!owner.startsWith("@") || owner.includes("/")) { + core.warning(`Skipping '${owner}' as emails and teams are not supported yet`); + continue; + } + uniqueOwners.add(owner.slice(1)); // remove the '@' + } + } + + return uniqueOwners; +} + +async function collectCurrentMaintainers(codeownersFiles, github, core) { + core.startGroup(`Fetching GitHub profile information for each codeowner`); + + const currentMaintainers = {}; + for (const codeowners of codeownersFiles) { + const owners = extractGitHubUsernames(codeowners.content, core); + + for (const owner of owners) { + if (config.ignoredUsers.includes(owner)) { + core.debug( + `[repo: ${codeowners.repo}]: The user '${owner}' is on the ignore list. Skipping...`, + ); + continue; + } + const key = owner.toLowerCase(); + if (!currentMaintainers[key]) { + // Fetching GitHub profile is useful to ensure that all maintainers are valid (e.g., their GitHub accounts haven't been deleted). + const profile = await getGitHubProfile(github, owner, core); + if (!profile) { + core.warning( + `[repo: ${codeowners.repo}]: GitHub profile not found for ${owner}.`, + ); + continue; + } + + currentMaintainers[key] = { ...profile, repos: [] }; + } + + currentMaintainers[key].repos.push(codeowners.repo); + } + } + + core.endGroup(); + return currentMaintainers; +} + +function refreshPreviousMaintainers( + previousMaintainers, + currentMaintainers, + core, +) { + core.startGroup(`Refreshing previous maintainers list`); + + const updatedMaintainers = []; + + // 1. Iterate over the list of previous maintainers to: + // - Remove any maintainers who are not listed in any current CODEOWNERS files. + // - Update the repos list, ensuring that other properties (e.g., 'linkedin', 'slack', etc.) remain unchanged. + for (const previousEntry of previousMaintainers) { + const key = previousEntry.github.toLowerCase(); + const currentMaintainer = currentMaintainers[key]; + if (!currentMaintainer) { + core.info( + `The previous ${previousEntry.github} maintainer was not found in any CODEOWNERS file. Removing...`, + ); + continue; + } + delete currentMaintainers[key]; + + updatedMaintainers.push({ + ...previousEntry, + repos: currentMaintainer.repos, + githubID: currentMaintainer.githubID, + }); + } + + // 2. Append new codeowners who are not present in the previous Maintainers file. + const newMaintainers = Object.values(currentMaintainers); + updatedMaintainers.push(...newMaintainers); + + core.endGroup(); + return updatedMaintainers; +} + +async function run(github, context, core) { + if (!config.maintainersFilePath) { + core.setFailed("The MAINTAINERS_FILE_PATH is not defined"); + return; + } + loadCache(core); + + const repos = await getRepositories( + github, + context.repo.owner, + config.ignoredRepos, + core, + ); + const codeownersFiles = await getAllCodeownersFiles( + github, + context.repo.owner, + repos, + core, + ); + + const previousMaintainers = yaml.load( + fs.readFileSync(config.maintainersFilePath, "utf8"), + ); + + // 1. Collect new maintainers from all current CODEOWNERS files found across all repositories. + const currentMaintainers = await collectCurrentMaintainers( + codeownersFiles, + github, + core, + ); + + // 2. Refresh the repository list for existing maintainers and add any new maintainers to the list. + const refreshedMaintainers = refreshPreviousMaintainers( + previousMaintainers, + currentMaintainers, + core, + ); + + fs.writeFileSync(config.maintainersFilePath, yaml.dump(refreshedMaintainers)); + + printAPICallsStats(core); + + await summarizeChanges(previousMaintainers, refreshedMaintainers, core); + saveCache(); +} diff --git a/.github/scripts/maintainers/summary.js b/.github/scripts/maintainers/summary.js new file mode 100644 index 000000000..e07d03fd4 --- /dev/null +++ b/.github/scripts/maintainers/summary.js @@ -0,0 +1,99 @@ +module.exports = { summarizeChanges }; + +async function summarizeChanges(oldMaintainers, newMaintainers, core) { + const outOfSync = []; + const noLongerActive = []; + + const newMaintainersByGitHubName = new Map(); + for (const newMaintainer of newMaintainers) { + newMaintainersByGitHubName.set(newMaintainer.github, newMaintainer); + } + + for (const oldEntry of oldMaintainers) { + const newEntry = newMaintainersByGitHubName.get(oldEntry.github); + + if (!newEntry) { + noLongerActive.push([oldEntry.github, repositoriesLinks(oldEntry.repos)]); + continue; + } + + const { newOwnedRepos, noLongerOwnedRepos } = compareRepos( + oldEntry.repos, + newEntry.repos, + ); + + if (newOwnedRepos.length > 0 || noLongerOwnedRepos.length > 0) { + outOfSync.push([ + profileLink(oldEntry.github), + repositoriesLinks(newOwnedRepos), + repositoriesLinks(noLongerOwnedRepos), + ]); + } + } + + if (outOfSync.length > 0) { + core.summary.addHeading("⚠️ Out of Sync Maintainers", "2"); + core.summary.addTable([ + [ + { data: "Name", header: true }, + { data: "Newly added to CODEOWNERS", header: true }, + { data: "No longer in CODEOWNERS", header: true }, + ], + ...outOfSync, + ]); + core.summary.addBreak(); + } + + if (noLongerActive.length > 0) { + core.summary.addHeading( + "👻 Inactive Maintainers (not listed in any repositories)", + "2", + ); + + core.summary.addTable([ + [ + { data: "Name", header: true }, + { data: "Previously claimed ownership in repos", header: true }, + ], + ...noLongerActive, + ]); + + core.summary.addBreak(); + } + + await core.summary.write({ overwrite: true }); +} + +function compareRepos(oldRepos, newRepos) { + const newOwnedRepositories = []; + const noLongerOwnedRepositories = []; + + for (const repo of newRepos) { + if (!oldRepos.includes(repo)) { + newOwnedRepositories.push(repo); + } + } + + for (const repo of oldRepos) { + if (!newRepos.includes(repo)) { + noLongerOwnedRepositories.push(repo); + } + } + + return { + newOwnedRepos: newOwnedRepositories, + noLongerOwnedRepos: noLongerOwnedRepositories, + }; +} + +function repositoriesLinks(repos) { + return repos + .map((repo) => { + return `${repo}`; + }) + .join(", "); +} + +function profileLink(login) { + return `${login}`; +} diff --git a/.github/workflows/update-maintainers.yaml b/.github/workflows/update-maintainers.yaml new file mode 100644 index 000000000..858eb02aa --- /dev/null +++ b/.github/workflows/update-maintainers.yaml @@ -0,0 +1,130 @@ +# This action updates the `MAINTAINERS.yaml` file based on `CODEOWNERS` files in all organization repositories. +# It is triggered when a `CODEOWNERS` file is changed; the related repository triggers this workflow by emitting the `trigger-maintainers-update` event. +# It can also be triggered manually. + +name: Update MAINTAINERS.yaml file + +on: + push: + branches: [ master ] + paths: + - 'CODEOWNERS' + - '.github/scripts/maintainers/**' + - '.github/workflows/update-maintainers.yaml' + + schedule: + - cron: "0 10 * * SUN" # Runs at 10:00 AM UTC every Sunday. + + workflow_dispatch: + + repository_dispatch: + types: [ trigger-maintainers-update ] + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +env: + IGNORED_REPOSITORIES: "shape-up-process" + IGNORED_USERS: "asyncapi-bot-eve" + + BRANCH_NAME: "bot/update-maintainers-${{ github.run_id }}" + PR_TITLE: "docs(maintainers): update MAINTAINERS.yaml file with the latest CODEOWNERS changes" + +jobs: + update-maintainers: + name: Update MAINTAINERS.yaml based on CODEOWNERS files in all organization repositories + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # If an action pushes code using the repository’s GITHUB_TOKEN, a pull request workflow will not run. + token: ${{ secrets.GH_TOKEN }} + + - 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 updating MAINTAINERS file." + git pull + exit 0 + fi + + sleep $INTERVAL + elapsed_time=$((elapsed_time + INTERVAL)) + done + + echo "Timeout reached. Proceeding with updating MAINTAINERS.yaml file with active pull request(s) present. It may result in merge conflict." + exit 0 + + - name: Restore cached GitHub API calls + uses: actions/cache/restore@v4 + with: + path: ./.github/scripts/maintainers/github.api.cache.json + key: github-api-cache + restore-keys: | + github-api-cache- + + - name: Installing Module + shell: bash + run: npm install js-yaml@4 --no-save + + - name: Run script updating MAINTAINERS.yaml + uses: actions/github-script@v7 + env: + GH_TOKEN: ${{ github.token }} + MAINTAINERS_FILE_PATH: "${{ github.workspace }}/MAINTAINERS.yaml" + with: + script: | + const script = require('./.github/scripts/maintainers/index.js') + await script({github, context, core}) + + - name: Save cached GitHub API calls + uses: actions/cache/save@v4 + with: + path: ./.github/scripts/maintainers/github.api.cache.json + # re-evaluate the key, so we update cache when file changes + key: github-api-cache-${{ hashfiles('./.github/scripts/maintainers/github.api.cache.json') }} + + - name: Create PR with latest changes + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # https://github.com/peter-evans/create-pull-request/releases/tag/v6.1.0 + with: + token: ${{ secrets.GH_TOKEN }} + commit-message: ${{ env.PR_TITLE }} + committer: asyncapi-bot + author: asyncapi-bot + title: ${{ env.PR_TITLE }} + branch: ${{ env.BRANCH_NAME }} + body: | + **Description** + - Update MAINTAINERS.yaml based on CODEOWNERS files across all repositories in the organization. + + For details on why a maintainer was removed or changed, refer to the [Job summary page](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}). + + - 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: 🚨 Update MAINTAINERS.yaml file Workflow failed 🚨 + SLACK_MESSAGE: Failed to auto update MAINTAINERS.yaml file. + MSG_MINIMAL: true diff --git a/MAINTAINERS.yaml b/MAINTAINERS.yaml index 29ad9ddb3..e9c443ce7 100644 --- a/MAINTAINERS.yaml +++ b/MAINTAINERS.yaml @@ -7,6 +7,9 @@ isTscMember: true repos: - website + - conference-website + - brand + githubID: 105395613 - name: Aayush Sahu github: aayushmau5 linkedin: aayushmau5 @@ -16,6 +19,7 @@ isTscMember: true repos: - diff + githubID: 54525741 - name: Abir Pal linkedin: imabp slack: U01S8EQ9LQ2 @@ -25,6 +29,7 @@ isTscMember: true repos: - problem + githubID: 53480076 - name: Akshat Nema github: akshatnema linkedin: akshat-nema @@ -34,6 +39,7 @@ isTscMember: true repos: - website + githubID: 76521428 - name: Ansh Goyal github: anshgoyalevil linkedin: thisisanshg @@ -43,6 +49,7 @@ isTscMember: true repos: - website + githubID: 94157520 - name: Anand Sunderraman github: anandsunderraman linkedin: anand-sunderraman-a6b7a131 @@ -51,6 +58,7 @@ isTscMember: true repos: - go-watermill-template + githubID: 4500774 - name: Ashish Padhy github: Shurtu-gal linkedin: ashish-padhy3023 @@ -60,6 +68,8 @@ isTscMember: true repos: - github-action-for-cli + - cli + githubID: 100484401 - name: Cameron Rushton github: CameronRushton slack: U01DVKKAV5K @@ -67,9 +77,11 @@ company: Solace isTscMember: true repos: + - spec-json-schemas + - bindings - java-spring-cloud-stream-template - python-paho-template - - bindings + githubID: 32455969 - name: Dale Lane github: dalelane linkedin: dalelane @@ -79,9 +91,12 @@ isTscMember: true company: IBM repos: - - avro-schema-parser + - spec + - spec-json-schemas - bindings + - avro-schema-parser - java-template + githubID: 1444788 - name: Emiliano Zublena github: emilianozublena linkedin: emilianozublena @@ -89,7 +104,8 @@ availableForHire: false isTscMember: false repos: - - asyncapi-php-template + - php-template + githubID: 466639 - name: Fran Méndez github: fmvilas slack: U34F2JRRS @@ -97,16 +113,21 @@ linkedin: fmvilas isTscMember: true repos: - - raml-dt-schema-parser - - avro-schema-parser - - openapi-schema-parser - - asyncapi-react - - glee - - nodejs-ws-template - - parser-js - spec - spec-json-schemas + - asyncapi-react + - extensions-catalog + - converter-js - bindings + - enterprise-patterns + - raml-dt-schema-parser + - openapi-schema-parser + - html-template + - markdown-template + - nodejs-ws-template + - generator-hooks + - brand + githubID: 242119 - name: Gerald Loeffler github: GeraldLoeffler linkedin: geraldloeffler @@ -114,7 +135,9 @@ availableForHire: false isTscMember: false repos: + - spec-json-schemas - bindings + githubID: 1985716 - name: Jonas Lagoni github: jonaslagoni linkedin: jonaslagoni @@ -123,14 +146,18 @@ company: Postman isTscMember: true repos: - - dotnet-nats-template + - spec-json-schemas + - generator + - parser-js + - converter-js - ts-nats-template + - dotnet-nats-template - generator-react-sdk - - generator - modelina - - parser-js - - parser-api - simulator + - parser-api + - EDAVisualiser + githubID: 13396189 - name: Khuda Dad Nomani github: KhudaDad414 twitter: KhudaDadNomani @@ -140,10 +167,12 @@ company: Postman isTscMember: true repos: - - bindings - - glee + - spec-json-schemas + - studio - .github - optimizer + - glee + githubID: 32505158 - name: Laurent Broudoux github: lbroudoux twitter: lbroudoux @@ -153,7 +182,9 @@ company: Postman isTscMember: true repos: + - spec-json-schemas - bindings + githubID: 1538635 - name: Ludovic Dussart github: M3lkior linkedin: ludovic-dussart-846a8063 @@ -164,6 +195,7 @@ isTscMember: true repos: - avro-schema-parser + githubID: 5501911 - name: Lukasz Gornicki github: derberg linkedin: lukasz-gornicki-a621914 @@ -173,17 +205,30 @@ company: Postman isTscMember: true repos: - - diff - - generator-filters - - generator-hooks - - github-action-for-generator + - spec + - website + - spec-json-schemas - generator + - asyncapi-react + - extensions-catalog + - bindings + - enterprise-patterns + - html-template + - markdown-template - nodejs-template - nodejs-ws-template - - spec - - spec-json-schemas + - java-spring-template + - github-action-for-cli + - .github + - jasyncapi + - generator-hooks + - vs-asyncapi-preview - template-for-generator-templates - - website + - community + - diff + - chatbot + - infra + githubID: 6995927 - name: Maciej Urbańczyk github: magicmatatjahu availableForHire: false @@ -192,18 +237,29 @@ company: Travelping GmbH isTscMember: true repos: + - website + - generator - asyncapi-react + - parser-go + - parser-js + - converter-js - converter-go - - generator-react-sdk - - generator + - studio - html-template - markdown-template + - github-action-for-cli + - ts-nats-template + - dotnet-nats-template + - template-for-generator-templates + - generator-react-sdk - modelina - - parser-js - - parser-go - - server-api - template-for-go-projects - - website + - diff + - chatbot + - server-api + - EDAVisualiser + - problem + githubID: 20404945 - name: Azeez Elegbede linkedin: acebuild github: AceTheCreator @@ -213,26 +269,20 @@ availableForHire: false isTscMember: true repos: + - conference-website - chatbot + githubID: 40604284 - name: Michael Davis github: damaru-inc availableForHire: false slack: UH3B166TD isTscMember: false repos: + - spec-json-schemas + - bindings - java-spring-cloud-stream-template - python-paho-template - - bindings -- name: Missy Turco - github: mcturco - twitter: missyturco - slack: U02JVEQ6S9W - linkedin: missy-turco-a476a6126 - availableForHire: false - company: Postman - isTscMember: false - repos: - - brand + githubID: 3926925 - name: Nektarios Fifes github: NektariosFifes linkedin: nektarios-fifes-372740220 @@ -241,6 +291,7 @@ isTscMember: true repos: - simulator + githubID: 61620751 - name: Pavel Bodiachevskii github: Pakisan slack: U0132LQU8C9 @@ -248,7 +299,11 @@ availableForHire: false isTscMember: true repos: + - spec-json-schemas + - tck - jasyncapi + - jasyncapi-idea-plugin + githubID: 3388414 - name: Philip Schlesinger github: theschles slack: U054UUYBNLF @@ -257,6 +312,7 @@ isTscMember: true repos: - jasyncapi-idea-plugin + githubID: 901430 - name: Prince Rajpoot github: princerajpoot20 linkedin: princerajpoot @@ -266,6 +322,7 @@ isTscMember: true repos: - studio + githubID: 44585452 - name: Richard Coppen github: rcoppen linkedin: richard-coppen @@ -274,7 +331,9 @@ company: IBM isTscMember: true repos: + - spec-json-schemas - bindings + githubID: 30902631 - name: Samir AMZANI github: Amzani slack: U01N6AW5V5G @@ -285,6 +344,8 @@ isTscMember: true repos: - studio + - cli + githubID: 554438 - name: Sergio Moya github: smoya linkedin: smoya @@ -296,17 +357,19 @@ repos: - spec - spec-json-schemas - - bindings - - parser-api - - parser-js - - avro-schema-parser - - openapi-schema-parser - - raml-dt-schema-parser - - server-api - parser-go + - parser-js - converter-go + - bindings + - raml-dt-schema-parser + - openapi-schema-parser + - avro-schema-parser - go-watermill-template - template-for-go-projects + - parser-api + - server-api + - infra + githubID: 1083296 - name: Souvik De github: Souvikns slack: U01SGCZMJKW @@ -317,8 +380,9 @@ isTscMember: true repos: - cli - - bundler - glee + - bundler + githubID: 41781438 - name: Quetzalli Writes github: quetzalliwrites twitter: QuetzalliWrites @@ -329,8 +393,7 @@ isTscMember: true repos: - website - - training - - community + githubID: 19964402 - name: David Pereira github: BOLT04 twitter: BOLT2938 @@ -341,6 +404,7 @@ isTscMember: true repos: - server-api + githubID: 18630253 - name: Daniel Raper github: dan-r slack: U02FP8WBFQE @@ -349,6 +413,7 @@ isTscMember: true repos: - java-template + githubID: 1384852 - name: Kieran Murphy github: KieranM1999 linkedin: kieran-murphy-175b0412b @@ -358,6 +423,7 @@ isTscMember: false repos: - java-template + githubID: 45017928 - name: Tom Jefferson github: JEFFLUFC linkedin: t-jefferson @@ -367,6 +433,7 @@ isTscMember: false repos: - java-template + githubID: 54025356 - name: Lewis Relph github: lewis-relph availableForHire: false @@ -375,6 +442,7 @@ isTscMember: false repos: - java-template + githubID: 91530893 - name: Semen Tenishchev github: Tenischev linkedin: semen-tenishchev @@ -383,6 +451,7 @@ isTscMember: true repos: - java-spring-template + githubID: 4137916 - name: Samridhi Agrawal github: Samridhi-98 slack: U02T2MY9W5T @@ -392,16 +461,7 @@ isTscMember: true repos: - modelina -- name: Debajyoti Halder - github: ron-debajyoti - twitter: rondebajyoti - slack: U02UK9RUPGQ - linkedin: rondebajyoti - availableForHire: false - company: Narvar - isTscMember: false - repos: - - modelina + githubID: 54466041 - name: Ivan Garcia Sainz-Aja github: ivangsa linkedin: ivangarciasainzaja @@ -411,6 +471,7 @@ isTscMember: true repos: - vs-asyncapi-preview + githubID: 1246876 - name: Florence Njeri github: Florence-Njeri linkedin: florencenjeri @@ -420,6 +481,7 @@ isTscMember: true repos: - generator + githubID: 40742916 - name: Jeremy Whitlock github: whitlockjc linkedin: whitlockjc @@ -429,7 +491,9 @@ company: Google isTscMember: true repos: + - spec-json-schemas - bindings + githubID: 98899 - name: Vladimír Gorej github: char0n linkedin: vladimirgorej @@ -439,18 +503,22 @@ company: SmartBear isTscMember: false repos: - - bindings - spec - spec-json-schemas + - bindings + githubID: 193286 - name: Alexander Wichmann - github: VisualBean + github: VisualBean linkedin: alexcarlsen slack: U04C58GB8TF availableForHire: false company: The LEGO Group isTscMember: true repos: + - spec-json-schemas - bindings + - saunter + githubID: 5294032 - name: Kenneth Aasan github: kennethaasan slack: U037S2HK4TS @@ -460,6 +528,7 @@ isTscMember: true repos: - modelina + githubID: 1437394 - name: Heiko Henning github: GreenRover slack: U03AC4G51H8 @@ -467,7 +536,11 @@ company: mtrail GmbH isTscMember: true repos: + - spec + - spec-json-schemas + - bindings - protobuf-schema-parser + githubID: 512850 - name: connil github: connil slack: U03A51H8 @@ -476,6 +549,7 @@ isTscMember: false repos: - dotnet-rabbitmq-template + githubID: 6583798 - name: mr-nuno github: mr-nuno slack: U03A5145 @@ -484,6 +558,7 @@ isTscMember: false repos: - dotnet-rabbitmq-template + githubID: 1067841 - name: Thulisile Sibanda github: thulieblack linkedin: v-thulisile-sibanda @@ -494,7 +569,9 @@ isTscMember: true repos: - website + - conference-website - community + githubID: 66913810 - name: Ashmit JaiSarita Gupta github: devilkiller-ag linkedin: jaisarita @@ -504,6 +581,7 @@ isTscMember: true repos: - modelina + githubID: 43639341 - name: Sambhav Gupta github: sambhavgupta0705 linkedin: sambhavgupta0705 @@ -513,11 +591,162 @@ isTscMember: true repos: - website + githubID: 81870866 - name: Viacheslav Turovskyi github: aeworxet slack: U01G3U01SVC availableForHire: false isTscMember: false repos: - - bundler - optimizer + - bundler + githubID: 16149591 +- name: Rohit + github: TRohit20 + twitter: TRRohit20 + isTscMember: false + repos: + - website + githubID: 108233235 +- name: 'Bhaswati Roy ' + github: BhaswatiRoy + twitter: swiftiebhaswati + isTscMember: false + repos: + - website + githubID: 78029145 +- name: 'Vaishnavi ' + github: VaishnaviNandakumar + isTscMember: false + repos: + - website + githubID: 41518119 +- name: Joy Almeida + github: J0SAL + twitter: _j0sal + isTscMember: false + repos: + - website + githubID: 52382282 +- name: Mihael Bosnjak + github: mboss37 + isTscMember: false + repos: + - spec-json-schemas + - bindings + githubID: 29606687 +- name: Steve Head + github: SrfHead + isTscMember: false + repos: + - spec-json-schemas + - bindings + githubID: 13767299 +- name: Dec Kolakowski + github: dpwdec + isTscMember: false + repos: + - spec-json-schemas + - bindings + githubID: 51292634 +- name: Ian Cooper + github: iancooper + twitter: ICooper + isTscMember: false + repos: + - spec-json-schemas + - bindings + githubID: 45537 +- name: Michael Wildman + github: m-wild + isTscMember: false + repos: + - saunter + githubID: 3260812 +- name: yurvon-screamo + github: yurvon-screamo + isTscMember: false + repos: + - saunter + githubID: 109030262 +- name: Jonathan Stoikovitch + github: jstoiko + twitter: jstoiko + isTscMember: false + repos: + - raml-dt-schema-parser + githubID: 9660342 +- name: Rishi + github: kaushik-rishi + twitter: KaushikRishi07 + isTscMember: false + repos: + - nodejs-template + githubID: 52498617 +- name: Akshit Gupta + github: akkshitgupta + twitter: akkshitgupta + availableForHire: true + isTscMember: false + repos: + - modelina + githubID: 96991785 +- name: Leigh Johnson + github: leigh-johnson + twitter: grepLeigh + isTscMember: false + repos: + - modelina + githubID: 2601819 +- name: Zbigniew Malcherczyk + github: ferror + isTscMember: false + repos: + - modelina + githubID: 17534504 +- name: artur-ciocanu + github: artur-ciocanu + isTscMember: false + repos: + - modelina + githubID: 743192 +- name: Vinit Shahdeo + github: vinitshahdeo + twitter: Vinit_Shahdeo + availableForHire: true + isTscMember: false + repos: + - diff + githubID: 20594326 +- name: Anubhav Vats + github: onbit-uchenik + twitter: postmanlabs + availableForHire: true + isTscMember: false + repos: + - diff + githubID: 46771418 +- name: Akshaya Gurlhosur + github: AGurlhosur + isTscMember: false + repos: + - java-template + githubID: 91530186 +- name: Philip Schlesinger @ Cryoport + github: philCryoport + isTscMember: false + repos: + - jasyncapi-idea-plugin + githubID: 28901899 +- name: nathanaelweber + github: nathanaelweber + isTscMember: false + repos: + - protobuf-schema-parser + githubID: 40006685 +- name: Barbanio González + github: Barbanio + isTscMember: false + repos: + - learning-paths + githubID: 77982319 diff --git a/tweets/recurring-slack-link/2024-08-24.tweet b/tweets/recurring-slack-link/2024-08-24.tweet new file mode 100644 index 000000000..0bd3e8865 --- /dev/null +++ b/tweets/recurring-slack-link/2024-08-24.tweet @@ -0,0 +1,7 @@ +✨ Did you know #AsyncAPI is on Slack? ✨ + +Join our Slack workspace to chat with anyone from our Open-Source community! + +🔗 asyncapi.com/slack-invite + +Ask for help and help others too. 💪🏿💪🏽🦾 \ No newline at end of file