Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
158 changes: 158 additions & 0 deletions .github/workflows/dogfood-comment-writer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
name: Add Dogfooding Comment Writer

on:
workflow_run:
workflows: ["Add Dogfooding Comment"]
types: [completed]

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

concurrency:
group: dogfood-comment-${{ github.event.workflow_run.head_repository.id || 'unknown-repo' }}-${{ github.event.workflow_run.head_branch || 'unknown-branch' }}
cancel-in-progress: true

jobs:
add-dogfood-comment:
if: ${{ github.repository_owner == 'CommunityToolkit' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Download comment result artifact
id: download-result
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dogfood-comment
path: ${{ runner.temp }}/dogfood-comment
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Add dogfooding comment
if: ${{ steps.download-result.outcome == 'success' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const fs = require('fs');
const path = require('path');

const workflowRun = context.payload.workflow_run;
const resultPath = path.join(process.env.RUNNER_TEMP, 'dogfood-comment', 'result.json');
const result = JSON.parse(fs.readFileSync(resultPath, 'utf8'));

function fail(message) {
throw new Error(`Invalid dogfood comment artifact: ${message}`);
}

if (result.schema_version !== 'dogfood-comment/v1') fail('unexpected schema_version');
if (result.event !== 'pull_request') fail('unexpected event');
if (!Number.isInteger(result.pr_number) || result.pr_number < 1) fail('invalid pr_number');
if (!/^[0-9a-f]{40}$/i.test(String(result.head_sha || ''))) fail('invalid head_sha');
if (String(result.run_id || '') !== String(workflowRun.id)) fail('run_id did not match workflow_run');
if (result.head_sha !== workflowRun.head_sha) fail('head_sha did not match workflow_run');

const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: result.pr_number,
});

if (pr.state !== 'open') {
core.info(`Skipping non-open PR #${result.pr_number}.`);
return;
}

const expectedBaseRepository = `${context.repo.owner}/${context.repo.repo}`.toLowerCase();
const runHeadRepository = String(workflowRun.head_repository?.full_name || '');
const runHeadRepositoryParts = runHeadRepository.split('/');
const runHeadRef = String(workflowRun.head_branch || '');
if (String(pr.base?.repo?.full_name || '').toLowerCase() !== expectedBaseRepository) {
fail(`PR #${result.pr_number} does not target this repository`);
}
if (pr.head.sha !== workflowRun.head_sha) {
core.warning(`Skipping stale comment for PR #${result.pr_number}: artifact head ${result.head_sha}, current head ${pr.head.sha}`);
return;
}
if (
runHeadRepositoryParts.length !== 2 ||
!runHeadRepositoryParts[0] ||
!runHeadRepositoryParts[1] ||
!runHeadRef ||
String(pr.head?.repo?.full_name || '').toLowerCase() !== runHeadRepository.toLowerCase() ||
String(pr.head?.ref || '') !== runHeadRef
) {
fail(`PR #${result.pr_number} head did not match workflow_run`);
}

const workflowRunPullRequests = Array.isArray(workflowRun.pull_requests) ? workflowRun.pull_requests : [];
if (workflowRunPullRequests.length > 0) {
if (!workflowRunPullRequests.some((pullRequest) => pullRequest.number === result.pr_number)) {
fail(`PR #${result.pr_number} was not present in workflow_run.pull_requests`);
}
} else {
const candidatePullRequests = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${runHeadRepositoryParts[0]}:${runHeadRef}`,
per_page: 100,
});
const trustedMatches = candidatePullRequests.filter((candidate) =>
candidate.head?.sha === workflowRun.head_sha &&
String(candidate.head?.ref || '') === runHeadRef &&
String(candidate.head?.repo?.full_name || '').toLowerCase() === runHeadRepository.toLowerCase() &&
String(candidate.base?.repo?.full_name || '').toLowerCase() === expectedBaseRepository
);
if (trustedMatches.length !== 1 || trustedMatches[0].number !== result.pr_number) {
fail(`PR #${result.pr_number} could not be uniquely associated with workflow_run`);
}
}

const prNumber = pr.number;
const bashScript = 'https://raw.githubusercontent.com/CommunityToolkit/Aspire/main/eng/scripts/dogfood-pr.sh';
const psScript = 'https://raw.githubusercontent.com/CommunityToolkit/Aspire/main/eng/scripts/dogfood-pr.ps1';
const dogfoodMarker = '<!-- dogfood-pr -->';
const comment = `${dogfoodMarker}
🚀 **Dogfood this PR with:**

> **⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.**

\`\`\`bash
curl -fsSL ${bashScript} | bash -s -- ${prNumber}
\`\`\`
Or
- Run remotely in PowerShell:
\`\`\`powershell
iex "& { $(irm ${psScript}) } ${prNumber}"
\`\`\``;
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
});
const existingComment = comments.find((comment) =>
comment.user?.login === 'github-actions[bot]' && String(comment.body || '').includes(dogfoodMarker)
);

if (existingComment) {
await github.rest.issues.updateComment({
comment_id: existingComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment,
});
return;
}

await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment,
});

- name: Note missing artifact
if: ${{ steps.download-result.outcome != 'success' }}
run: echo "No dogfood-comment artifact was available; nothing to synchronize."
44 changes: 34 additions & 10 deletions .github/workflows/dogfood-comment.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
name: Add Dogfooding Comment

on:
# Use pull_request_target to run in the context of the base branch
# This allows commenting on PRs from forks
pull_request_target:
pull_request:
types: [opened, reopened, synchronize]
branches:
- 'main'
# Allow manual triggering
workflow_dispatch:
inputs:
pr_number:
Expand All @@ -16,19 +13,46 @@ on:
type: number

jobs:
add-dogfood-comment:
# Only run on the CommunityToolkit org to avoid running on forks
if: ${{ github.repository_owner == 'CommunityToolkit' }}
prepare-dogfood-comment:
if: ${{ github.repository_owner == 'CommunityToolkit' && github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
permissions:
pull-requests: write
pull-requests: read
steps:
- name: Write comment result artifact
env:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
mkdir -p "$RUNNER_TEMP/dogfood-comment"
jq -n \
--arg schema_version "dogfood-comment/v1" \
--arg event "pull_request" \
--arg head_sha "$PR_HEAD_SHA" \
--argjson pr_number "$PR_NUMBER" \
--arg run_id "$GITHUB_RUN_ID" \
'{schema_version:$schema_version,event:$event,head_sha:$head_sha,pr_number:$pr_number,run_id:$run_id}' \
> "$RUNNER_TEMP/dogfood-comment/result.json"

- name: Upload comment result artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dogfood-comment
path: ${{ runner.temp }}/dogfood-comment/result.json
if-no-files-found: error
retention-days: 3

add-dogfood-comment-manually:
if: ${{ github.repository_owner == 'CommunityToolkit' && github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Add dogfooding comment
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
// Get PR number from either the PR event or manual input
const prNumber = context.payload.number || context.payload.inputs.pr_number;
const prNumber = context.payload.inputs.pr_number;
const bashScript = 'https://raw.githubusercontent.com/CommunityToolkit/Aspire/main/eng/scripts/dogfood-pr.sh';
const psScript = 'https://raw.githubusercontent.com/CommunityToolkit/Aspire/main/eng/scripts/dogfood-pr.ps1';

Expand Down
156 changes: 156 additions & 0 deletions .github/workflows/integration-codeowner-writer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
name: Integration code owner prompt writer

on:
workflow_run:
workflows: ["Integration code owner automation"]
types: [completed]

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

concurrency:
group: integration-codeowner-prompt-${{ github.event.workflow_run.head_repository.id || 'unknown-repo' }}-${{ github.event.workflow_run.head_branch || 'unknown-branch' }}
cancel-in-progress: true

jobs:
prompt-for-codeowner:
if: ${{ github.repository_owner == 'CommunityToolkit' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Download prompt result artifact
id: download-result
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: integration-codeowner-prompt
path: ${{ runner.temp }}/integration-codeowner-prompt
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Prompt for code owner interest
if: ${{ steps.download-result.outcome == 'success' }}
uses: actions/github-script@v9.0.0
with:
script: |
const fs = require('fs');
const path = require('path');

const workflowRun = context.payload.workflow_run;
const resultPath = path.join(process.env.RUNNER_TEMP, 'integration-codeowner-prompt', 'result.json');
const result = JSON.parse(fs.readFileSync(resultPath, 'utf8'));

function fail(message) {
throw new Error(`Invalid integration code owner prompt artifact: ${message}`);
}

if (result.schema_version !== 'integration-codeowner-prompt/v1') fail('unexpected schema_version');
if (result.event !== 'pull_request') fail('unexpected event');
if (!Number.isInteger(result.pr_number) || result.pr_number < 1) fail('invalid pr_number');
if (typeof result.is_new_integration !== 'boolean') fail('invalid is_new_integration');
if (!/^[0-9a-f]{40}$/i.test(String(result.head_sha || ''))) fail('invalid head_sha');
if (String(result.run_id || '') !== String(workflowRun.id)) fail('run_id did not match workflow_run');
if (result.head_sha !== workflowRun.head_sha) fail('head_sha did not match workflow_run');

if (!result.is_new_integration) {
core.info('Skipping PR because it does not add a new integration.');
return;
}

const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: result.pr_number,
});

if (pr.state !== 'open' || pr.draft) {
core.info(`Skipping non-open or draft PR #${result.pr_number}.`);
return;
}

const expectedBaseRepository = `${context.repo.owner}/${context.repo.repo}`.toLowerCase();
const runHeadRepository = String(workflowRun.head_repository?.full_name || '');
const runHeadRepositoryParts = runHeadRepository.split('/');
const runHeadRef = String(workflowRun.head_branch || '');
if (String(pr.base?.repo?.full_name || '').toLowerCase() !== expectedBaseRepository) {
fail(`PR #${result.pr_number} does not target this repository`);
}
if (pr.head.sha !== workflowRun.head_sha) {
core.warning(`Skipping stale prompt for PR #${result.pr_number}: artifact head ${result.head_sha}, current head ${pr.head.sha}`);
return;
}
if (
runHeadRepositoryParts.length !== 2 ||
!runHeadRepositoryParts[0] ||
!runHeadRepositoryParts[1] ||
!runHeadRef ||
String(pr.head?.repo?.full_name || '').toLowerCase() !== runHeadRepository.toLowerCase() ||
String(pr.head?.ref || '') !== runHeadRef
) {
fail(`PR #${result.pr_number} head did not match workflow_run`);
}

const workflowRunPullRequests = Array.isArray(workflowRun.pull_requests) ? workflowRun.pull_requests : [];
if (workflowRunPullRequests.length > 0) {
if (!workflowRunPullRequests.some((pullRequest) => pullRequest.number === result.pr_number)) {
fail(`PR #${result.pr_number} was not present in workflow_run.pull_requests`);
}
} else {
const candidatePullRequests = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${runHeadRepositoryParts[0]}:${runHeadRef}`,
per_page: 100,
});
const trustedMatches = candidatePullRequests.filter((candidate) =>
candidate.head?.sha === workflowRun.head_sha &&
String(candidate.head?.ref || '') === runHeadRef &&
String(candidate.head?.repo?.full_name || '').toLowerCase() === runHeadRepository.toLowerCase() &&
String(candidate.base?.repo?.full_name || '').toLowerCase() === expectedBaseRepository
);
if (trustedMatches.length !== 1 || trustedMatches[0].number !== result.pr_number) {
fail(`PR #${result.pr_number} could not be uniquely associated with workflow_run`);
}
}

const marker = '<!-- integration-codeowner-prompt -->';
const body = `${marker}
Thanks for opening a new integration PR, @${pr.user.login}.

If you want to become the code owner for this integration, reply with:
- \`/codeowner-accept\`
- \`/codeowner-reject\`

Only the PR author should use these commands.`;
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
per_page: 100,
});
const existingComment = comments.find((comment) =>
comment.user?.login === 'github-actions[bot]' && String(comment.body || '').includes(marker)
);

if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body,
});
return;
}

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body,
});

- name: Note missing artifact
if: ${{ steps.download-result.outcome != 'success' }}
run: echo "No integration-codeowner-prompt artifact was available; nothing to synchronize."
Loading
Loading