From 916ff8880857c4155115df8184f10ab182ee73e8 Mon Sep 17 00:00:00 2001 From: NiftyAndy Date: Sun, 2 Aug 2026 14:41:27 -0400 Subject: [PATCH] fix(draft-pr): fall back to workflow token --- .github/workflows/draft-pr.yml | 60 +++++++++++++++++++++++----------- test/cli.test.mjs | 39 +++++++++++++++++++--- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/.github/workflows/draft-pr.yml b/.github/workflows/draft-pr.yml index 39cfcb0..f353ff8 100644 --- a/.github/workflows/draft-pr.yml +++ b/.github/workflows/draft-pr.yml @@ -18,9 +18,6 @@ permissions: contents: read pull-requests: write -env: - HAS_AUTOMATION_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN != '' || secrets.RELEASE_PLEASE_TOKEN != '' }} - jobs: create-draft-pr: name: Create @@ -34,7 +31,7 @@ jobs: id: check-pr env: HEAD_REF: ${{ github.ref_name }} - GH_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN || secrets.RELEASE_PLEASE_TOKEN || github.token }} + GH_TOKEN: ${{ github.token }} run: | BRANCH="$HEAD_REF" EXISTING=$(gh pr list --repo "$GITHUB_REPOSITORY" --limit 1 --base staging --head "$BRANCH" --state open --json number,title 2>/dev/null | node -e 'let d=""; process.stdin.on("data", c => d += c).on("end", () => console.log(JSON.parse(d || "[]").length))') @@ -43,9 +40,11 @@ jobs: - name: Create if: steps.check-pr.outputs.existing == '0' env: + AUTOMATION_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN || secrets.RELEASE_PLEASE_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} HEAD_REF: ${{ github.ref_name }} - GH_TOKEN: ${{ secrets.CODE_FOUNDRY_TOKEN || secrets.RELEASE_PLEASE_TOKEN || github.token }} run: | + set -euo pipefail BRANCH="$HEAD_REF" PREFIX="${BRANCH%%/*}" SUFFIX="${BRANCH#*/}" @@ -71,19 +70,42 @@ jobs: PR_TITLE="$BRANCH" fi - DRAFT_ARGS=() - if [ "$HAS_AUTOMATION_TOKEN" = true ]; then - BODY_NOTICE="This PR is ready for review and merging once required validation passes." - else - DRAFT_ARGS+=(--draft) - BODY_NOTICE="This PR was created automatically and is currently in draft mode." - echo "::notice title=Manual PR readiness required::No automation token is configured; this PR may remain draft until manually marked ready for validation and merge." + BODY_FILE=$(mktemp) + ERROR_FILE=$(mktemp) + trap 'rm -f -- "$BODY_FILE" "$ERROR_FILE"' EXIT + cat > "$BODY_FILE" < /dev/null 2> "$ERROR_FILE"; then + CREATED=true + fi + + if [ "$CREATED" != true ]; then + DRAFT_ARGS=("${CREATE_ARGS[@]}" --field draft=true) + if GH_TOKEN="$GITHUB_TOKEN" gh api "${DRAFT_ARGS[@]}" > /dev/null 2> "$ERROR_FILE"; then + echo "::notice title=Manual PR readiness required::The configured automation token was absent or rejected; this PR was opened as a draft with the short-lived workflow token." + CREATED=true + fi fi - gh pr create \ - --repo "$GITHUB_REPOSITORY" \ - --base staging \ - --head "$BRANCH" \ - "${DRAFT_ARGS[@]}" \ - --title "$PR_TITLE" \ - --body "CI PR created automatically by this workflow.\n\n $BODY_NOTICE\n\n **CI Status:** Pending \u2014 this workflow triggered on push to \`$BRANCH\`." + if [ "$CREATED" != true ]; then + echo "::error title=Draft PR creation failed::Both the configured automation token and the workflow token failed to create the pull request." + exit 1 + fi diff --git a/test/cli.test.mjs b/test/cli.test.mjs index 22a70c1..301952b 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -729,10 +729,12 @@ describe('code-foundry CLI', () => { const draftCallee = readFileSync('.github/workflows/draft-pr.yml', 'utf8') assert.match(draftCallee, /CODE_FOUNDRY_TOKEN:\n\s+required: false/) assert.match(draftCallee, /RELEASE_PLEASE_TOKEN:\n\s+required: false/) - assert.match(draftCallee, /HAS_AUTOMATION_TOKEN: \$\{\{ secrets\.CODE_FOUNDRY_TOKEN != '' \|\| secrets\.RELEASE_PLEASE_TOKEN != '' \}\}/) - assert.match(draftCallee, /if \[ "\$HAS_AUTOMATION_TOKEN" = true \]; then/) - assert.match(draftCallee, /DRAFT_ARGS=\(\)/) - assert.match(draftCallee, /\$\{DRAFT_ARGS\[\@\]\}/) + assert.match(draftCallee, /GH_TOKEN: \$\{\{ github\.token \}\}/) + assert.match(draftCallee, /AUTOMATION_TOKEN: \$\{\{ secrets\.CODE_FOUNDRY_TOKEN \|\| secrets\.RELEASE_PLEASE_TOKEN \}\}/) + assert.match(draftCallee, /GITHUB_TOKEN: \$\{\{ github\.token \}\}/) + assert.match(draftCallee, /CREATE_ARGS=\(/) + assert.match(draftCallee, /DRAFT_ARGS=\("\$\{CREATE_ARGS\[@\]\}" --field draft=true\)/) + assert.doesNotMatch(draftCallee, /gh pr create/) const draftCaller = readFileSync('.github/workflows/draft-pr_self-ci.yml', 'utf8') assert.match(draftCaller, /secrets:\n\s+CODE_FOUNDRY_TOKEN: \$\{\{ secrets\.CODE_FOUNDRY_TOKEN \}\}/) @@ -749,6 +751,35 @@ describe('code-foundry CLI', () => { const validationCaller = readFileSync('.github/workflows/validation_self-ci.yml', 'utf8') assert.match(validationCaller, /types:\n\s+- opened\n\s+- synchronize\n\s+- reopened\n\s+- ready_for_review/) }) + it('creates draft PRs through REST and falls back from rejected automation tokens', () => { + const workflow = readFileSync('.github/workflows/draft-pr.yml', 'utf8') + const stepSlice = (name) => { + const start = workflow.indexOf(`- name: ${name}\n`) + assert.ok(start !== -1, `workflow has a ${name} step`) + const next = workflow.indexOf('- name: ', start + 1) + return next === -1 ? workflow.slice(start) : workflow.slice(start, next) + } + + const checkStep = stepSlice('Check') + assert.match(checkStep, /GH_TOKEN: \$\{\{ github\.token \}\}/) + assert.doesNotMatch(checkStep, /CODE_FOUNDRY_TOKEN|RELEASE_PLEASE_TOKEN/) + + const createStep = stepSlice('Create') + assert.match(createStep, /AUTOMATION_TOKEN: \$\{\{ secrets\.CODE_FOUNDRY_TOKEN \|\| secrets\.RELEASE_PLEASE_TOKEN \}\}/) + assert.match(createStep, /GITHUB_TOKEN: \$\{\{ github\.token \}\}/) + assert.match(createStep, /"repos\/\$\{GITHUB_REPOSITORY\}\/pulls"/) + assert.match(createStep, /--method POST/) + assert.match(createStep, /--field base=staging/) + assert.match(createStep, /--field head="\$BRANCH"/) + assert.match(createStep, /--field title="\$PR_TITLE"/) + assert.match(createStep, /--field body="@\$BODY_FILE"/) + assert.match(createStep, /GH_TOKEN="\$AUTOMATION_TOKEN" gh api "\$\{CREATE_ARGS\[@\]\}"/) + assert.match(createStep, /DRAFT_ARGS=\("\$\{CREATE_ARGS\[@\]\}" --field draft=true\)/) + assert.match(createStep, /GH_TOKEN="\$GITHUB_TOKEN" gh api "\$\{DRAFT_ARGS\[@\]\}"/) + assert.match(createStep, /Manual PR readiness required/) + assert.doesNotMatch(createStep, /echo "\$AUTOMATION_TOKEN"|printenv|GITHUB_OUTPUT/) + assert.doesNotMatch(workflow, /gh pr create/) + }) it('fails closed on a non-squash release merge strategy and never uses --admin', () => { const workflow = readFileSync('.github/workflows/release.yml', 'utf8')