mirror of
https://github.com/we-promise/sure.git
synced 2026-06-04 10:19:03 +00:00
* ci(preview): rewrite image config before registry push Point the trusted preview deploy config at the loaded CI image before Wrangler validates the worker config for the Cloudflare registry push. This keeps the existing trusted deploy boundary intact while fixing the post-2062 image-push ordering regression. * ci(preview): require trusted readiness diagnostics * ci(preview): use nonce for diagnostics events * ci(preview): retain diagnostics timing anchors
514 lines
20 KiB
YAML
514 lines
20 KiB
YAML
name: Deploy PR Preview
|
|
|
|
on:
|
|
workflow_run:
|
|
workflows: ["Pull Request"]
|
|
types: [completed]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
preview-gate:
|
|
if: |
|
|
github.event.workflow_run.event == 'pull_request' &&
|
|
github.event.workflow_run.conclusion == 'success'
|
|
name: Validate preview deployment gates
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
pull-requests: read
|
|
outputs:
|
|
artifact_name: ${{ steps.preview.outputs.artifact_name }}
|
|
head_sha: ${{ steps.preview.outputs.head_sha }}
|
|
is_fork: ${{ steps.preview.outputs.is_fork }}
|
|
pr_number: ${{ steps.preview.outputs.pr_number }}
|
|
should_deploy: ${{ steps.preview.outputs.should_deploy }}
|
|
|
|
steps:
|
|
- name: Checkout trusted preview resolver
|
|
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
with:
|
|
ref: ${{ github.event.repository.default_branch }}
|
|
path: trusted-preview-resolver
|
|
persist-credentials: false
|
|
sparse-checkout: |
|
|
workers/preview/deploy
|
|
|
|
- name: Resolve preview request
|
|
id: preview
|
|
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
|
with:
|
|
script: |
|
|
const { resolvePreviewRequest } = require('./trusted-preview-resolver/workers/preview/deploy/resolve_preview_request.cjs');
|
|
await resolvePreviewRequest({ github, context, core });
|
|
|
|
deployment_record:
|
|
needs: preview-gate
|
|
if: needs.preview-gate.outputs.should_deploy == 'true'
|
|
name: Create GitHub Deployment
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
contents: read
|
|
deployments: write
|
|
outputs:
|
|
deployment_id: ${{ steps.deployment.outputs.result }}
|
|
env:
|
|
HEAD_SHA: ${{ needs.preview-gate.outputs.head_sha }}
|
|
IS_FORK: ${{ needs.preview-gate.outputs.is_fork }}
|
|
PR_NUMBER: ${{ needs.preview-gate.outputs.pr_number }}
|
|
|
|
steps:
|
|
- name: Create GitHub Deployment
|
|
if: env.IS_FORK == 'false'
|
|
id: deployment
|
|
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
|
with:
|
|
script: |
|
|
const prNumber = process.env.PR_NUMBER;
|
|
const headSha = process.env.HEAD_SHA;
|
|
const deployment = await github.rest.repos.createDeployment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
ref: headSha,
|
|
environment: `preview-pr-${prNumber}`,
|
|
auto_merge: false,
|
|
required_contexts: [],
|
|
description: 'PR Preview Deployment'
|
|
});
|
|
return deployment.data.id;
|
|
result-encoding: string
|
|
|
|
deploy-preview:
|
|
needs: [preview-gate, deployment_record]
|
|
if: |
|
|
always() &&
|
|
needs.preview-gate.outputs.should_deploy == 'true' &&
|
|
(needs.deployment_record.result == 'success' || needs.deployment_record.result == 'skipped')
|
|
name: Deploy to Cloudflare Containers
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
concurrency:
|
|
group: preview-deploy-${{ needs.preview-gate.outputs.pr_number }}
|
|
cancel-in-progress: true
|
|
environment: preview
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
outputs:
|
|
preview_url: ${{ steps.deploy.outputs.preview_url }}
|
|
env:
|
|
ARTIFACT_NAME: ${{ needs.preview-gate.outputs.artifact_name }}
|
|
HEAD_SHA: ${{ needs.preview-gate.outputs.head_sha }}
|
|
IS_FORK: ${{ needs.preview-gate.outputs.is_fork }}
|
|
PR_NUMBER: ${{ needs.preview-gate.outputs.pr_number }}
|
|
|
|
steps:
|
|
- name: Checkout trusted preview tooling
|
|
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
with:
|
|
ref: ${{ github.event.repository.default_branch }}
|
|
path: trusted
|
|
persist-credentials: false
|
|
sparse-checkout: |
|
|
workers/preview
|
|
|
|
- name: Download preview image artifact
|
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
|
with:
|
|
name: ${{ env.ARTIFACT_NAME }}
|
|
run-id: ${{ github.event.workflow_run.id }}
|
|
github-token: ${{ github.token }}
|
|
path: ${{ runner.temp }}/preview-image
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: Verify preview image artifact checksum
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
image_archive="$RUNNER_TEMP/preview-image/sure-preview-image.tar.gz"
|
|
checksum_file="$RUNNER_TEMP/preview-image/sure-preview-image.sha256"
|
|
manifest_file="$RUNNER_TEMP/preview-image/sure-preview-image.manifest.json"
|
|
expected_files="$(mktemp)"
|
|
actual_files="$(mktemp)"
|
|
|
|
test -f "$image_archive"
|
|
test -f "$checksum_file"
|
|
test -f "$manifest_file"
|
|
|
|
printf '%s\n' \
|
|
sure-preview-image.manifest.json \
|
|
sure-preview-image.sha256 \
|
|
sure-preview-image.tar.gz | sort > "$expected_files"
|
|
find "$RUNNER_TEMP/preview-image" -maxdepth 1 -type f -printf '%f\n' | sort > "$actual_files"
|
|
|
|
if ! diff -u "$expected_files" "$actual_files"; then
|
|
echo "Preview image artifact contained unexpected files" >&2
|
|
exit 1
|
|
fi
|
|
|
|
expected_checksum="$(tr -d '[:space:]' < "$checksum_file")"
|
|
actual_checksum="$(sha256sum "$image_archive" | awk '{print $1}')"
|
|
|
|
if [ "$expected_checksum" != "$actual_checksum" ]; then
|
|
echo "Preview image artifact checksum mismatch" >&2
|
|
exit 1
|
|
fi
|
|
|
|
node - "$manifest_file" "$expected_checksum" <<'NODE'
|
|
const fs = require('node:fs');
|
|
|
|
const manifestPath = process.argv[2];
|
|
const expectedChecksum = process.argv[3];
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
const expectedImageTag = `sure-preview-pr-${process.env.PR_NUMBER}:${process.env.HEAD_SHA}`;
|
|
const expected = {
|
|
artifactVersion: 1,
|
|
archivePath: 'sure-preview-image.tar.gz',
|
|
archiveSha256: expectedChecksum,
|
|
headSha: process.env.HEAD_SHA,
|
|
imageTag: expectedImageTag,
|
|
prNumber: process.env.PR_NUMBER,
|
|
};
|
|
|
|
for (const [key, value] of Object.entries(expected)) {
|
|
if (manifest[key] !== value) {
|
|
throw new Error(`Preview image manifest ${key} mismatch`);
|
|
}
|
|
}
|
|
|
|
if (!/^sha256:[a-f0-9]{64}$/.test(manifest.imageId || '')) {
|
|
throw new Error('Preview image manifest imageId is invalid');
|
|
}
|
|
NODE
|
|
|
|
- name: Prepare trusted preview deploy workspace
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
preview_dir="$RUNNER_TEMP/sure-preview-worker"
|
|
rm -rf "$preview_dir"
|
|
mkdir -p "$preview_dir"
|
|
|
|
cp trusted/workers/preview/package.json "$preview_dir/package.json"
|
|
cp trusted/workers/preview/package-lock.json "$preview_dir/package-lock.json"
|
|
cp trusted/workers/preview/tsconfig.json "$preview_dir/tsconfig.json"
|
|
cp trusted/workers/preview/wrangler.toml "$preview_dir/wrangler.toml"
|
|
cp -R trusted/workers/preview/src "$preview_dir/src"
|
|
|
|
diagnostics_nonce="$(openssl rand -hex 32)"
|
|
sed -i "s/\${PR_NUMBER}/${PR_NUMBER}/g" "$preview_dir/wrangler.toml"
|
|
sed -i "s/\${PR_NUMBER}/${PR_NUMBER}/g" "$preview_dir/src/index.ts"
|
|
sed -i "s/\${PREVIEW_DIAGNOSTICS_NONCE}/${diagnostics_nonce}/g" "$preview_dir/src/index.ts"
|
|
|
|
if grep -F "\${PREVIEW_DIAGNOSTICS_NONCE}" "$preview_dir/src/index.ts" >/dev/null; then
|
|
echo "Preview diagnostics nonce placeholder was not replaced" >&2
|
|
exit 1
|
|
fi
|
|
|
|
cd "$preview_dir"
|
|
npm ci --ignore-scripts --no-audit --no-fund
|
|
|
|
- name: Load preview image artifact
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
image_archive="$RUNNER_TEMP/preview-image/sure-preview-image.tar.gz"
|
|
manifest_file="$RUNNER_TEMP/preview-image/sure-preview-image.manifest.json"
|
|
expected_image="sure-preview-pr-${PR_NUMBER}:${HEAD_SHA}"
|
|
|
|
gzip -dc "$image_archive" | docker load
|
|
docker image inspect "$expected_image" >/dev/null
|
|
expected_image_id="$(node -e 'const fs = require("node:fs"); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1], "utf8")).imageId);' "$manifest_file")"
|
|
actual_image_id="$(docker image inspect --format '{{.Id}}' "$expected_image")"
|
|
|
|
if [ "$expected_image_id" != "$actual_image_id" ]; then
|
|
echo "Loaded preview image ID did not match artifact manifest" >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Push preview image to Cloudflare registry
|
|
id: image
|
|
env:
|
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_PREVIEW_API_TOKEN || secrets.CLOUDFLARE_API_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
cd "$RUNNER_TEMP/sure-preview-worker"
|
|
config_path="$RUNNER_TEMP/sure-preview-worker/wrangler.toml"
|
|
image_tag="sure-preview-pr-${PR_NUMBER}:${HEAD_SHA}"
|
|
push_log="$RUNNER_TEMP/wrangler-containers-push.log"
|
|
clean_log="$RUNNER_TEMP/wrangler-containers-push.clean.log"
|
|
|
|
# wrangler containers push validates wrangler.toml, so point the trusted
|
|
# config at the loaded CI image before replacing it with the registry ref.
|
|
LOCAL_IMAGE_TAG="$image_tag" node - "$config_path" <<'NODE'
|
|
const fs = require('node:fs');
|
|
|
|
const configPath = process.argv[2];
|
|
const imageTag = process.env.LOCAL_IMAGE_TAG;
|
|
|
|
if (!/^sure-preview-pr-[1-9][0-9]*:[a-f0-9]{40}$/.test(imageTag || '')) {
|
|
throw new Error('Expected local preview image tag for wrangler containers push');
|
|
}
|
|
|
|
const original = fs.readFileSync(configPath, 'utf8');
|
|
const updated = original.replace(/image = "[^"]+"/, `image = ${JSON.stringify(imageTag)}`);
|
|
if (updated === original) {
|
|
throw new Error('Expected wrangler.toml to contain an image entry to rewrite before push');
|
|
}
|
|
fs.writeFileSync(configPath, updated);
|
|
NODE
|
|
|
|
./node_modules/.bin/wrangler containers push "$image_tag" 2>&1 | tee "$push_log"
|
|
perl -pe 's/\e\[[0-9;]*[A-Za-z]//g' "$push_log" > "$clean_log"
|
|
image_ref=$(grep -Eo 'registry\.cloudflare\.com/[^[:space:]]+' "$clean_log" | tail -n 1 | tr -d '\r')
|
|
|
|
if [ -z "$image_ref" ]; then
|
|
echo "Could not find Cloudflare registry image reference in wrangler output" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "image_ref=${image_ref}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Configure trusted preview image reference
|
|
env:
|
|
IMAGE_REF: ${{ steps.image.outputs.image_ref }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
config_path="$RUNNER_TEMP/sure-preview-worker/wrangler.toml"
|
|
# Use Node instead of sed so the replacement preserves TOML string syntax.
|
|
node - "$config_path" <<'NODE'
|
|
const fs = require('node:fs');
|
|
|
|
const configPath = process.argv[2];
|
|
const imageRef = process.env.IMAGE_REF;
|
|
|
|
if (!imageRef || !imageRef.startsWith('registry.cloudflare.com/')) {
|
|
throw new Error('Expected a Cloudflare registry image reference');
|
|
}
|
|
|
|
const original = fs.readFileSync(configPath, 'utf8');
|
|
const updated = original.replace(/image = "[^"]+"/, `image = ${JSON.stringify(imageRef)}`);
|
|
if (updated === original) {
|
|
throw new Error('Expected wrangler.toml to contain an image entry to rewrite');
|
|
}
|
|
fs.writeFileSync(configPath, updated);
|
|
NODE
|
|
|
|
cat "$config_path"
|
|
|
|
- name: Deploy to Cloudflare Containers
|
|
id: deploy
|
|
env:
|
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_PREVIEW_API_TOKEN || secrets.CLOUDFLARE_API_TOKEN }}
|
|
CLOUDFLARE_WORKERS_SUBDOMAIN: ${{ secrets.CLOUDFLARE_WORKERS_SUBDOMAIN }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
cd "$RUNNER_TEMP/sure-preview-worker"
|
|
deploy_log="$RUNNER_TEMP/wrangler-deploy.log"
|
|
clean_deploy_log="$RUNNER_TEMP/wrangler-deploy.clean.log"
|
|
|
|
deploy_once() {
|
|
./node_modules/.bin/wrangler deploy --config wrangler.toml --var "PR_NUMBER:${PR_NUMBER}" 2>&1 | tee "$deploy_log"
|
|
}
|
|
|
|
if ! deploy_once; then
|
|
perl -pe 's/\e\[[0-9;]*[A-Za-z]//g' "$deploy_log" > "$clean_deploy_log"
|
|
|
|
if grep -F "associated with a different durable object namespace" "$clean_deploy_log" >/dev/null; then
|
|
echo "Detected stale Cloudflare container app state for PR ${PR_NUMBER}; deleting preview Worker and retrying once."
|
|
if ! ./node_modules/.bin/wrangler delete --name "sure-preview-${PR_NUMBER}" --force; then
|
|
echo "Preview Worker delete failed; continuing to the single retry so wrangler deploy reports the final error if the stale state remains." >&2
|
|
fi
|
|
deploy_once
|
|
else
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Get the deployment URL
|
|
PREVIEW_URL="https://sure-preview-${PR_NUMBER}.${CLOUDFLARE_WORKERS_SUBDOMAIN}.workers.dev"
|
|
echo "preview_url=${PREVIEW_URL}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Warm preview container
|
|
env:
|
|
PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
|
|
run: |
|
|
echo "Triggering preview wake-up..."
|
|
curl -fsS --connect-timeout 5 --max-time 15 "$PREVIEW_URL/_container_status" >/dev/null || true
|
|
|
|
- name: Collect preview diagnostics
|
|
if: success()
|
|
env:
|
|
PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
diagnostics_file="$RUNNER_TEMP/preview-diagnostics.json"
|
|
last_error=""
|
|
|
|
for attempt in $(seq 1 40); do
|
|
if curl -fsS --connect-timeout 5 --max-time 15 "$PREVIEW_URL/_container_status" -o "$diagnostics_file"; then
|
|
if jq -e '.previewReady == true or .previewFailed == true' "$diagnostics_file" >/dev/null; then
|
|
break
|
|
fi
|
|
else
|
|
last_error="curl failed on attempt ${attempt}"
|
|
fi
|
|
|
|
sleep 3
|
|
done
|
|
|
|
if [ ! -s "$diagnostics_file" ]; then
|
|
jq -n --arg error "${last_error:-preview diagnostics unavailable}" \
|
|
--arg url "$PREVIEW_URL" \
|
|
'{previewReady: false, previewFailed: false, error: $error, previewUrl: $url}' > "$diagnostics_file"
|
|
fi
|
|
|
|
jq -c . "$diagnostics_file"
|
|
|
|
if jq -e '.previewFailed == true' "$diagnostics_file" >/dev/null; then
|
|
echo "Preview diagnostics from _container_status reported previewFailed=true:" >&2
|
|
jq -c . "$diagnostics_file" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! jq -e '.previewReady == true' "$diagnostics_file" >/dev/null; then
|
|
echo "Preview diagnostics from _container_status did not reach previewReady=true:" >&2
|
|
jq -c . "$diagnostics_file" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! jq -e '.timings.previewReadyAt != null and .timings.secondsToPreviewReady != null' "$diagnostics_file" >/dev/null; then
|
|
echo "Preview diagnostics are missing readiness timing fields:" >&2
|
|
jq -c . "$diagnostics_file" >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Upload preview diagnostics
|
|
if: always() && steps.deploy.outputs.preview_url != ''
|
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
|
with:
|
|
name: preview-diagnostics-pr-${{ env.PR_NUMBER }}-${{ env.HEAD_SHA }}
|
|
path: ${{ runner.temp }}/preview-diagnostics.json
|
|
if-no-files-found: error
|
|
retention-days: 3
|
|
|
|
- name: Store cleanup metadata
|
|
if: success()
|
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
|
with:
|
|
name: preview-cleanup-pr-${{ env.PR_NUMBER }}
|
|
path: ${{ runner.temp }}/sure-preview-worker/wrangler.toml
|
|
retention-days: 2
|
|
|
|
deployment_status:
|
|
needs: [preview-gate, deployment_record, deploy-preview]
|
|
if: |
|
|
always() &&
|
|
needs.preview-gate.outputs.should_deploy == 'true' &&
|
|
needs.preview-gate.outputs.is_fork == 'false' &&
|
|
needs.deployment_record.result == 'success'
|
|
name: Update GitHub Deployment Status
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
contents: read
|
|
deployments: write
|
|
env:
|
|
DEPLOYMENT_ID: ${{ needs.deployment_record.outputs.deployment_id }}
|
|
DEPLOY_RESULT: ${{ needs.deploy-preview.result }}
|
|
PREVIEW_URL: ${{ needs.deploy-preview.outputs.preview_url }}
|
|
|
|
steps:
|
|
- name: Update Deployment Status
|
|
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
|
with:
|
|
script: |
|
|
const state = process.env.DEPLOY_RESULT === 'success' ? 'success' : 'failure';
|
|
const previewUrl = process.env.PREVIEW_URL || undefined;
|
|
await github.rest.repos.createDeploymentStatus({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
deployment_id: Number(process.env.DEPLOYMENT_ID),
|
|
state: state,
|
|
environment_url: state === 'success' ? previewUrl : undefined,
|
|
description: state === 'success' ? 'Preview deployed successfully' : 'Preview deployment failed'
|
|
});
|
|
|
|
preview_comment:
|
|
needs: [preview-gate, deploy-preview]
|
|
if: needs.deploy-preview.result == 'success'
|
|
name: Comment on PR
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
env:
|
|
HEAD_SHA: ${{ needs.preview-gate.outputs.head_sha }}
|
|
PR_NUMBER: ${{ needs.preview-gate.outputs.pr_number }}
|
|
PREVIEW_URL: ${{ needs.deploy-preview.outputs.preview_url }}
|
|
|
|
steps:
|
|
- name: Comment on PR
|
|
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
|
with:
|
|
script: |
|
|
const previewUrl = process.env.PREVIEW_URL;
|
|
const issueNumber = Number(process.env.PR_NUMBER);
|
|
const headSha = process.env.HEAD_SHA;
|
|
const commentBody = [
|
|
'## 🚀 Preview Deployment Ready',
|
|
'',
|
|
"Your preview environment has been deployed to Cloudflare Containers with the PR's Docker image.",
|
|
'',
|
|
`**Preview URL:** ${previewUrl}`,
|
|
'',
|
|
'> ⏰ This preview is intended to be cleaned up after **24 hours** of the last deployment once the cleanup workflow is live on the default branch.',
|
|
'> 💤 The container will sleep after 30 minutes of inactivity and wake on the next request.',
|
|
'',
|
|
'---',
|
|
`<sub>Deployed from commit ${headSha}</sub>`,
|
|
].join('\n');
|
|
|
|
// Find existing comment
|
|
const { data: comments } = await github.rest.issues.listComments({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issueNumber
|
|
});
|
|
|
|
const botComment = comments.find(comment =>
|
|
comment.user.type === 'Bot' &&
|
|
comment.body.includes('Preview Deployment Ready')
|
|
);
|
|
|
|
if (botComment) {
|
|
await github.rest.issues.updateComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: botComment.id,
|
|
body: commentBody
|
|
});
|
|
} else {
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issueNumber,
|
|
body: commentBody
|
|
});
|
|
}
|