Skip to content

Commit

Permalink
Update PR workflow script to account for non-member ok-to-test labels (
Browse files Browse the repository at this point in the history
  • Loading branch information
librarian authored Mar 6, 2025
1 parent ee3601a commit f908d28
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 24 deletions.
132 changes: 130 additions & 2 deletions .github/workflows/pr-github-actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ on:
- "yes"
- "no"

permissions:
contents: read
pull-requests: write
issues: write

defaults:
run:
shell: bash
Expand All @@ -68,8 +73,125 @@ jobs:
- name: set env
id: set-env
run: echo
check-running-allowed:
runs-on: ubuntu-latest
outputs:
result: ${{ steps.check-ownership-membership.outputs.result }}
steps:
- name: Check if running tests is allowed
id: check-ownership-membership
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
script: |
async function isOrgMember(owner, username) {
core.info(`Checking membership for user: ${username}`);
try {
const { data: membership } = await github.rest.orgs.getMembershipForUser({
org: owner,
username
});
if (membership?.state === 'active') {
core.info(`${username} is confirmed as an org member`);
return true;
} else {
core.info(`${username} is not an active org member (state: ${membership?.state})`);
return false;
}
} catch (error) {
// Often the call fails if the user isn't in the org or it's private
core.error(`Error checking membership for user ${username}: ${error.message}`);
return false;
}
}
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request.number;
const prAuthor = context.payload.pull_request.user.login;
core.info(`Starting membership check for PR #${prNumber} by @${prAuthor}`);
const authorIsOrgMember = await isOrgMember(owner, prAuthor);
if (authorIsOrgMember) {
core.info(`User @${prAuthor} is org member => authorized`);
return true;
}
core.info(`User @${prAuthor} is NOT an org member; checking for 'ok-to-test' label`);
let events;
try {
const resp = await github.rest.issues.listEvents({
owner,
repo,
issue_number: prNumber
});
events = resp.data;
} catch (error) {
core.error(`Error fetching issue events: ${error.message}`);
return false;
}
// Find last 'labeled' event for 'ok-to-test'
const labeledOkToTest = events
.filter(e => e.event === 'labeled' && e.label?.name === 'ok-to-test')
.pop();
if (!labeledOkToTest) {
core.info("No 'ok-to-test' label found on this PR");
return false;
}
core.info(`Found 'ok-to-test' label event by @${labeledOkToTest.actor.login}, verifying if they are an org member...`);
const labelerLogin = labeledOkToTest.actor.login;
const labelerIsOrgMember = await isOrgMember(owner, labelerLogin);
if (!labelerIsOrgMember) {
core.info(`User @${labelerLogin} who labeled 'ok-to-test' is not an org member => not authorized`);
return false;
}
core.info(`'ok-to-test' label added by an org member => authorized`);
return true
- name: comment-if-waiting-on-ok
if: steps.check-ownership-membership.outputs.result == 'false' &&
github.event.action == 'opened'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Hi! Thank you for contributing!\nThe tests on this PR will run after a maintainer adds an `ok-to-test` label to this PR manually. Thank you for your patience!'
});
- name: cleanup-labels
uses: actions/github-script@v7
with:
script: |
let labelsToRemove = ['ok-to-test', 'recheck'];
const prNumber = context.payload.pull_request.number;
const prLabels = new Set(context.payload.pull_request.labels.map(l => l.name));
for await (const label of labelsToRemove.filter(l => prLabels.has(l))) {
core.info(`remove label=${label} for pr=${prNumber}`);
try {
const result = await github.rest.issues.removeLabel({
...context.repo,
issue_number: prNumber,
name: label
});
} catch(error) {
// ignore the 404 error that arises
// when the label did not exist for the
// organization member
if (error.status && error.status != 404) {
throw error;
}
}
}
shell:
needs: [set-env, check-running-allowed]
if: needs.check-running-allowed.outputs.result == 'true'
runs-on: ubuntu-latest

steps:
Expand Down Expand Up @@ -117,6 +239,8 @@ jobs:
shfmt_flags: "-i 4 -ci -kp -bn -sr"

python:
needs: [set-env, check-running-allowed]
if: needs.check-running-allowed.outputs.result == 'true'
runs-on: ubuntu-latest

steps:
Expand Down Expand Up @@ -159,6 +283,8 @@ jobs:
reporter: ${{ steps.reporter.outputs.value }}

workflows:
needs: [set-env, check-running-allowed]
if: needs.check-running-allowed.outputs.result == 'true'
runs-on: ubuntu-latest
steps:
- name: checkout
Expand Down Expand Up @@ -196,15 +322,17 @@ jobs:
body: '${{ steps.lint.outputs.WORKFLOW_LINT }}'
})
nbs-github-actions:
needs: [set-env, check-running-allowed]
if: needs.check-running-allowed.outputs.result == 'true'
name: Launch scripts on test-data
uses: ./.github/workflows/github_actions_scripts.yaml
secrets: inherit

create-and-delete-vm:
needs: [set-env, check-running-allowed]
if: needs.check-running-allowed.outputs.result == 'true'
name: Create and delete VM
uses: ./.github/workflows/create_and_delete_vm.yaml
needs:
- set-env
with:
nebius: ${{ needs.set-env.outputs.nebius }}
allow_downgrade: ${{ needs.set-env.outputs.allow_downgrade }}
Expand Down
86 changes: 64 additions & 22 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ on:
- 'synchronize'
- 'reopened'
- 'labeled'

permissions:
contents: read
pull-requests: write
issues: write

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
check-running-allowed:
runs-on: ubuntu-latest
Expand All @@ -29,40 +36,75 @@ jobs:
with:
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
script: |
// How to interpret membership status code:
// https://docs.github.com/en/rest/orgs/members?apiVersion=2022-11-28#check-organization-membership-for-a-user
const userLogin = context.payload.pull_request.user.login;
const isOrgMember = async function () {
async function isOrgMember(owner, username) {
core.info(`Checking membership for user: ${username}`);
try {
const response = await github.rest.orgs.checkMembershipForUser({
org: context.payload.organization.login,
username: userLogin,
const { data: membership } = await github.rest.orgs.getMembershipForUser({
org: owner,
username
});
return response.status == 204;
} catch (error) {
if (error.status && error.status == 404) {
if (membership?.state === 'active') {
core.info(`${username} is confirmed as an org member`);
return true;
} else {
core.info(`${username} is not an active org member (state: ${membership?.state})`);
return false;
}
throw error;
} catch (error) {
// Often the call fails if the user isn't in the org or it's private
core.error(`Error checking membership for user ${username}: ${error.message}`);
return false;
}
}
if (context.payload.repository.owner.login == userLogin) {
console.log('User is repo owner')
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request.number;
const prAuthor = context.payload.pull_request.user.login;
core.info(`Starting membership check for PR #${prNumber} by @${prAuthor}`);
const authorIsOrgMember = await isOrgMember(owner, prAuthor);
if (authorIsOrgMember) {
core.info(`User @${prAuthor} is org member => authorized`);
return true;
}
if (await isOrgMember()) {
console.log('User is member')
return true;
core.info(`User @${prAuthor} is NOT an org member; checking for 'ok-to-test' label`);
let events;
try {
const resp = await github.rest.issues.listEvents({
owner,
repo,
issue_number: prNumber
});
events = resp.data;
} catch (error) {
core.error(`Error fetching issue events: ${error.message}`);
return false;
}
// Find last 'labeled' event for 'ok-to-test'
const labeledOkToTest = events
.filter(e => e.event === 'labeled' && e.label?.name === 'ok-to-test')
.pop();
if (!labeledOkToTest) {
core.info("No 'ok-to-test' label found on this PR");
return false;
}
core.info(`Found 'ok-to-test' label event by @${labeledOkToTest.actor.login}, verifying if they are an org member...`);
const labelerLogin = labeledOkToTest.actor.login;
const labelerIsOrgMember = await isOrgMember(owner, labelerLogin);
if (!labelerIsOrgMember) {
core.info(`User @${labelerLogin} who labeled 'ok-to-test' is not an org member => not authorized`);
return false;
}
const labels = context.payload.pull_request.labels;
const okToTestLabel = labels.find(
label => label.name == 'ok-to-test'
);
return okToTestLabel !== undefined;
core.info(`'ok-to-test' label added by an org member => authorized`);
return true
- name: comment-if-waiting-on-ok
if: steps.check-ownership-membership.outputs.result == 'false' &&
github.event.action == 'opened'
Expand Down

0 comments on commit f908d28

Please sign in to comment.