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
60 changes: 41 additions & 19 deletions .github/workflows/draft-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))')
Expand All @@ -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#*/}"
Expand All @@ -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" <<EOF
CI PR created automatically by this workflow.

This PR is ready for review once required validation passes. If the
configured automation token is rejected, it is intentionally opened
as a draft for manual readiness.

**CI Status:** Pending — this workflow triggered on push to $BRANCH.
EOF

CREATE_ARGS=(
"repos/${GITHUB_REPOSITORY}/pulls"
--method POST
--field base=staging
--field head="$BRANCH"
--field title="$PR_TITLE"
--field body="@$BODY_FILE"
)

CREATED=false
if [ -n "$AUTOMATION_TOKEN" ] && GH_TOKEN="$AUTOMATION_TOKEN" gh api "${CREATE_ARGS[@]}" > /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
39 changes: 35 additions & 4 deletions test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 \}\}/)
Expand All @@ -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')

Expand Down
Loading