Skip to content
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6d2a491
Add GitHub Action to trigger internal ADO CI on /test comment
udsmicrosoft Apr 16, 2026
ba9cf9c
Merge pull request #2 from udsmicrosoft/users/urismiley/external-ci-mvp
udsmicrosoft Apr 21, 2026
2686d07
Fix duplicate env keys in internal-ci-trigger workflow
udsmicrosoft Apr 21, 2026
381f48d
Drop reaction step that requires issues:write permission
udsmicrosoft Apr 21, 2026
e1cd5bb
Restore reaction step with issues:write permission
udsmicrosoft Apr 21, 2026
f92509f
Use write-all permissions to debug reaction step 403
udsmicrosoft Apr 21, 2026
f531aab
Strip sha256= prefix from ADO webhook signature header
udsmicrosoft Apr 21, 2026
45877e4
Capture ADO webhook response body for debugging
udsmicrosoft Apr 21, 2026
6835791
Use SHA1 for ADO webhook HMAC signature (matches ADO verification)
udsmicrosoft Apr 21, 2026
0129e67
Pass triggerer (commenter) as separate field in webhook payload
udsmicrosoft Apr 22, 2026
1aa0d03
Skip internal CI when PR only changes docs/CI files (with /test force…
udsmicrosoft Apr 22, 2026
0b3b189
Align internal CI status context
udsmicrosoft May 5, 2026
641c552
Use external CI webhook service connection URL
udsmicrosoft May 12, 2026
0ac056f
Use configured incoming webhook name
udsmicrosoft May 12, 2026
628743b
Use unique ADO webhook endpoint
udsmicrosoft May 12, 2026
8a23b80
Harden internal CI trigger workflow
udsmicrosoft May 19, 2026
736e8ed
Make internal CI comment reaction non-blocking
udsmicrosoft May 19, 2026
db67bc1
Send comment-shaped internal CI webhooks
udsmicrosoft May 19, 2026
05eeaae
Create external CI demo PR
udsmicrosoft May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 281 additions & 0 deletions .github/workflows/internal-ci-trigger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
# MVP: Trigger internal ADO pipeline from GitHub PR comments.
# Production version will add: private repo ref push, CredScan redaction,
# and fork PR support.
name: Internal CI Trigger
run-name: "Internal CI for PR #${{ github.event.issue.number }}"

on:
issue_comment:
types: [created]

jobs:
trigger-internal-ci:
# Only run on PR comments (not issue comments) that are /test commands.
if: >-
github.event.issue.pull_request
&& (
github.event.comment.body == '/test'
|| startsWith(github.event.comment.body, '/test ')
)
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: read
statuses: write
steps:
- name: Validate test command
env:
COMMENT_BODY: ${{ github.event.comment.body }}
run: |
if [[ ! "$COMMENT_BODY" =~ ^/test([[:space:]]+force)?[[:space:]]*$ ]]; then
echo "::error::Unsupported internal CI command. Use /test or /test force."
exit 1
fi

- name: Check authorization
id: auth
env:
GH_TOKEN: ${{ github.token }}
MICROSOFT_GITHUB_ORG: microsoft
MS_ORG_READ_TOKEN: ${{ secrets.MS_ORG_READ_TOKEN }}
run: |
COMMENTER="${{ github.event.comment.user.login }}"

# Check if the commenter has write access to the repo
PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${COMMENTER}/permission" \
--jq '.permission')

if [[ "$PERMISSION" != "admin" && "$PERMISSION" != "write" ]]; then
echo "::error::User ${COMMENTER} does not have write access (permission: ${PERMISSION})"
exit 1
fi

if [[ -z "${MS_ORG_READ_TOKEN:-}" ]]; then
echo "::error::MS_ORG_READ_TOKEN secret is required to verify Microsoft org membership"
exit 1
fi

if ! GH_TOKEN="$MS_ORG_READ_TOKEN" gh api "orgs/${MICROSOFT_GITHUB_ORG}/members/${COMMENTER}" --silent; then
echo "::error::User ${COMMENTER} is not a member of ${MICROSOFT_GITHUB_ORG}, or membership could not be verified"
exit 1
fi

echo "authorized=true" >> "$GITHUB_OUTPUT"
echo "User ${COMMENTER} authorized (permission: ${PERMISSION}, org: ${MICROSOFT_GITHUB_ORG})"

- name: React to comment
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \
-f content='eyes' --silent

- name: Get PR details and pinned SHA
id: pr
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER="${{ github.event.issue.number }}"

PR_DATA=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}" \
--jq '{sha: .head.sha, branch: .head.ref, head_repo: .head.repo.full_name, base_repo: .base.repo.full_name}')

COMMIT_SHA=$(echo "$PR_DATA" | jq -r '.sha')
SOURCE_BRANCH=$(echo "$PR_DATA" | jq -r '.branch')
HEAD_REPO=$(echo "$PR_DATA" | jq -r '.head_repo')
BASE_REPO=$(echo "$PR_DATA" | jq -r '.base_repo')

# MVP: reject fork PRs — they require additional handling
if [[ "$HEAD_REPO" != "$BASE_REPO" ]]; then
echo "::error::Fork PRs are not supported yet (head: ${HEAD_REPO}, base: ${BASE_REPO})"
exit 1
fi

echo "commit_sha=${COMMIT_SHA}" >> "$GITHUB_OUTPUT"
echo "source_branch=${SOURCE_BRANCH}" >> "$GITHUB_OUTPUT"
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"

echo "PR #${PR_NUMBER} at SHA ${COMMIT_SHA} (branch: ${SOURCE_BRANCH})"

- name: Path filter — skip if only irrelevant files changed
id: pathfilter
env:
GH_TOKEN: ${{ github.token }}
COMMENT_BODY: ${{ github.event.comment.body }}
run: |
PR_NUMBER="${{ steps.pr.outputs.pr_number }}"

# Allow override: `/test force` bypasses the path filter
if echo "$COMMENT_BODY" | grep -qiE '^/test[[:space:]]+force\b'; then
echo "Path filter bypassed via '/test force'"
echo "skip=false" >> "$GITHUB_OUTPUT"
exit 0
fi

# Files matching any of these patterns are considered irrelevant to internal CI.
# If the PR touches *only* these, we skip the expensive run.
IGNORE_PATTERNS=(
'*.md'
'**/*.md'
'LICENSE'
'NOTICE'
'CODEOWNERS'
'MAINTAINERS.md'
'.gitignore'
'.gitattributes'
'docs/**'
'.github/**'
'rfcs/**'
'licenses/**'
)

# Get list of changed files (paginated, up to 3000)
mapfile -t FILES < <(gh api --paginate \
"repos/${{ github.repository }}/pulls/${PR_NUMBER}/files" \
--jq '.[].filename')

if [[ ${#FILES[@]} -eq 0 ]]; then
echo "::warning::No files reported changed; running CI anyway"
echo "skip=false" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "Changed files (${#FILES[@]}):"
printf ' %s\n' "${FILES[@]}"

shopt -s globstar extglob nullglob
RELEVANT=0
for f in "${FILES[@]}"; do
matched=0
for pat in "${IGNORE_PATTERNS[@]}"; do
# shellcheck disable=SC2053
if [[ "$f" == $pat ]]; then
matched=1
break
fi
done
if [[ $matched -eq 0 ]]; then
echo "Relevant file: $f"
RELEVANT=1
break
fi
done

if [[ $RELEVANT -eq 0 ]]; then
echo "All changed files match ignore patterns — skipping internal CI"
echo "skip=true" >> "$GITHUB_OUTPUT"

gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
-f body="⏭️ Internal CI skipped: PR only touches docs/CI/license files. Use \`/test force\` to override." \
--silent
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Set pending commit status
if: steps.pathfilter.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api "repos/${{ github.repository }}/statuses/${{ steps.pr.outputs.commit_sha }}" \
-f state='pending' \
-f description='Internal CI triggered, waiting for results...' \
-f context='ms-internal-ci/oss-jstests' \
--silent

- name: POST webhook to ADO
if: steps.pathfilter.outputs.skip != 'true'
env:
MS_WEBHOOK_SECRET: ${{ secrets.MS_WEBHOOK_SECRET }}
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER="${{ steps.pr.outputs.pr_number }}"
COMMIT_SHA="${{ steps.pr.outputs.commit_sha }}"
SOURCE_BRANCH="${{ steps.pr.outputs.source_branch }}"

if [[ -z "${MS_WEBHOOK_SECRET:-}" ]]; then
echo "::error::MS_WEBHOOK_SECRET secret is required to trigger ADO"
exit 1
fi

# Build a comment-shaped payload so each /test comment is a distinct
# webhook event while still carrying the pinned PR SHA for ADO.
PAYLOAD=$(jq -n \
--arg action "created" \
--arg pr_number "$PR_NUMBER" \
--arg sha "$COMMIT_SHA" \
--arg branch "$SOURCE_BRANCH" \
--arg repo "${{ github.repository }}" \
--arg triggerer "${{ github.event.comment.user.login }}" \
--arg comment_id "${{ github.event.comment.id }}" \
--arg comment_url "${{ github.event.comment.html_url }}" \
--arg comment_created_at "${{ github.event.comment.created_at }}" \
'{
action: $action,
number: ($pr_number | tonumber),
triggerer: $triggerer,
comment: {
id: ($comment_id | tonumber),
html_url: $comment_url,
created_at: $comment_created_at,
user: {
login: $triggerer
}
},
issue: {
number: ($pr_number | tonumber),
pull_request: {
url: "https://api.github.com/repos/\($repo)/pulls/\($pr_number)"
}
},
repository: {
full_name: $repo
},
pull_request: {
html_url: "https://github.com/\($repo)/pull/\($pr_number)",
head: {
sha: $sha,
ref: $branch,
repo: {
full_name: $repo
}
},
base: {
ref: "main"
},
user: {
login: $triggerer
}
}
}')

# Compute the raw HMAC-SHA1 hex signature expected by the ADO
# documentdb-oss-external-ci-webhook service connection. Unsigned
# and bad-signature requests must fail before queuing ADO pipeline 56298.
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha1 -hmac "$MS_WEBHOOK_SECRET" -binary | xxd -p -c 256)

# POST to ADO incoming webhook endpoint
HTTP_STATUS=$(curl -sS -o /dev/null -w "%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-H "X-GitHub-ADO-Signature: $SIGNATURE" \
-d "$PAYLOAD" \
"https://dev.azure.com/msdata/_apis/public/distributedtask/webhooks/documentdb-oss-external-ci-webhook?api-version=6.0-preview")

echo "ADO webhook response: HTTP ${HTTP_STATUS}"

if [[ "$HTTP_STATUS" -lt 200 || "$HTTP_STATUS" -ge 300 ]]; then
echo "::error::ADO webhook POST failed with HTTP ${HTTP_STATUS}"

# Set error status so the PR doesn't stay pending forever
gh api "repos/${{ github.repository }}/statuses/${COMMIT_SHA}" \
-f state='error' \
-f description='Failed to trigger internal CI pipeline' \
-f context='ms-internal-ci/oss-jstests' \
--silent
exit 1
fi

echo "Webhook triggered successfully"
Loading