Skip to content

Automated sync from private repo (2026-06-20) #156

Automated sync from private repo (2026-06-20)

Automated sync from private repo (2026-06-20) #156

name: Redirect Pull Requests
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
jobs:
redirect:
runs-on: ubuntu-latest
steps:
- name: Check org membership and redirect
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const author = pr.user.login;
// Allow PRs from trusted automation bots (e.g., repo sync)
const allowedBots = ['foundry-samples-repo-sync[bot]'];
if (allowedBots.includes(author)) {
console.log(`Skipping redirect for allowed bot: ${author}`);
return;
}
// Classify the PR author as internal (Microsoft) vs external using a
// cascade of signals. The GITHUB_TOKEN is an *installation* token, not
// a user identity in the 'microsoft' or 'microsoft-foundry' orgs, so
// the org-membership checks below can only confirm *public* members.
// Most Microsoft employees default to private membership, so we also
// fall back to a username pattern and a public-profile heuristic.
// Contributors with no public Microsoft signal anywhere will still be
// misclassified as external; the external-tier message below carries a
// universal caveat pointing self-aware internal contributors at the
// private staging repo, so that failure mode is self-correcting.
async function classifyAuthor(login) {
// Signal 1: microsoft-foundry org membership (public members only).
try {
const res = await github.rest.orgs.checkMembershipForUser({
org: 'microsoft-foundry',
username: login,
});
if (res.status === 204) return 'microsoft-foundry org member (public)';
} catch {}
// Signal 2: direct collaborator on this repo (team-based access is
// typically not visible to GITHUB_TOKEN here).
try {
const res = await github.rest.repos.checkCollaborator({
owner: context.repo.owner,
repo: context.repo.repo,
username: login,
});
if (res.status === 204) return 'repo collaborator';
} catch {}
// Signal 3: microsoft org membership (public members only).
try {
const res = await github.rest.orgs.checkMembershipForUser({
org: 'microsoft',
username: login,
});
if (res.status === 204) return 'microsoft org member (public)';
} catch {}
// Signal 4: username pattern. Matches 'ms', 'msft', or 'microsoft'
// as a whole token bounded by start/end/'-'/'_'. Catches handles like
// 'aprilk-ms', 'mitsha-microsoft', 'brandom-msft' without false-
// positiving 'cosmos', 'awesome', etc.
if (/(^|[-_])(ms|msft|microsoft)([-_]|$)/i.test(login)) {
return 'username pattern';
}
// Signal 5: public profile heuristic. Strict regex on `email`, plus
// a normalized whole-string match on `company` against a small allow
// list. We deliberately do NOT scan `bio` — phrases like
// 'ex-Microsoft' or 'Microsoft MVP' would produce false positives.
try {
const { data: profile } = await github.rest.users.getByUsername({ username: login });
const email = (profile.email || '').trim();
if (/@([a-z0-9-]+\.)?microsoft\.com$/i.test(email)) {
return 'profile email (@microsoft.com)';
}
const normalizedCompany = (profile.company || '')
.trim()
.toLowerCase()
.replace(/^@/, '')
.replace(/[.,]+$/, '');
const acceptedCompanies = new Set([
'microsoft',
'microsoft corporation',
'microsoft corp',
'msft',
]);
if (acceptedCompanies.has(normalizedCompany)) {
return 'profile company';
}
} catch {}
return null;
}
const matchedSignal = await classifyAuthor(author);
const isInternal = matchedSignal !== null;
console.log(`Author: ${author}, isInternal: ${isInternal}, signal: ${matchedSignal || 'none'}`);
let body;
if (isInternal) {
body = [
`👋 Thanks for your contribution, @${author}!`,
'',
'This repository is read-only. If you are contributing on behalf of Microsoft, please submit your PR to the private staging repository instead:',
'',
'👉 **[foundry-samples-pr](https://github.com/microsoft-foundry/foundry-samples-pr)**',
'',
'See [CONTRIBUTING.md](https://github.com/microsoft-foundry/foundry-samples/blob/main/CONTRIBUTING.md) for full instructions.',
].join('\n');
} else {
body = [
`👋 Thanks for your interest in contributing, @${author}!`,
'',
'This repository does not accept pull requests directly. If you\'d like to report a bug, suggest an improvement, or propose a new sample, please **[open an issue](https://github.com/microsoft-foundry/foundry-samples/issues/new)** instead.',
'',
'_If you are a Microsoft-internal contributor, please submit your PR through **[foundry-samples-pr](https://github.com/microsoft-foundry/foundry-samples-pr)** instead._',
'',
'See [CONTRIBUTING.md](https://github.com/microsoft-foundry/foundry-samples/blob/main/CONTRIBUTING.md) for more details.',
].join('\n');
}
// Skip if the bot already commented (idempotent on re-runs). We
// match on the staging-repo slug "microsoft-foundry/foundry-samples-pr",
// which both the internal- and external-tier messages above include
// and is structurally specific to this workflow — generic phrases
// like "This repository" can collide with unrelated bot comments
// and silently suppress the redirect.
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
});
const alreadyCommented = comments.data.some(c =>
c.user.login === 'github-actions[bot]' &&
c.body.includes('microsoft-foundry/foundry-samples-pr')
);
if (alreadyCommented) {
console.log('Bot already commented on this PR, skipping.');
return;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body,
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
state: 'closed',
});