diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3092a03..067f3298 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,10 @@ name: Release on: workflow_dispatch: + inputs: + release-pr: + description: a release PR number to rerun release jobs on + type: string push: branches: - main @@ -53,7 +57,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - npx --offline template-oss-release-please ${{ github.ref_name }} ${{ github.event_name }} + npx --offline template-oss-release-please "${{ github.ref_name }}" "${{ inputs.release-pr }}" - name: Post Pull Request Comment if: steps.release.outputs.pr-number uses: actions/github-script@v6 @@ -76,7 +80,7 @@ jobs: body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Update This Release\n\n` body += `This PR will be updated and CI will run for every non-\`chore:\` commit that is pushed to \`main\`. ` body += `To force CI to update this PR, run this command:\n\n` - body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo}\n\`\`\`` + body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo} -f release-pr=${issue_number}\n\`\`\`` if (commentId) { await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body }) @@ -378,10 +382,14 @@ jobs: with: script: | const { PR_NUMBER: issue_number, RESULT } = process.env - const { repo: { owner, repo } } = context + const { runId, repo: { owner, repo } } = context const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - const updateComment = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith('## Release Workflow\n\n')) + const updateComment = comments.find(c => + c.user.login === 'github-actions[bot]' && + c.body.startsWith('## Release Workflow\n\n') && + c.body.includes(runId) + ) if (updateComment) { console.log('Found comment to update:', JSON.stringify(updateComment, null, 2)) diff --git a/bin/release-please.js b/bin/release-please.js index 17c39442..6237d02b 100755 --- a/bin/release-please.js +++ b/bin/release-please.js @@ -4,7 +4,7 @@ const core = require('@actions/core') const main = require('../lib/release-please/index.js') const dryRun = !process.env.CI -const [branch, eventName] = process.argv.slice(2) +const [branch, forcePullRequest] = process.argv.slice(2) const debugPr = (val) => { if (dryRun) { @@ -45,7 +45,7 @@ main({ repo: process.env.GITHUB_REPOSITORY, dryRun, branch, - force: eventName === 'workflow_dispatch', + forcePullRequest: forcePullRequest ? +forcePullRequest : null, }).then(({ pr, release, releases }) => { if (pr) { debugPr(pr) diff --git a/lib/content/release.yml b/lib/content/release.yml index c0c73323..9c3f0f57 100644 --- a/lib/content/release.yml +++ b/lib/content/release.yml @@ -2,6 +2,10 @@ name: Release on: workflow_dispatch: + inputs: + release-pr: + description: a release PR number to rerun release jobs on + type: string push: branches: {{#each branches}} @@ -30,7 +34,7 @@ jobs: env: GITHUB_TOKEN: $\{{ secrets.GITHUB_TOKEN }} run: | - {{ rootNpxPath }} --offline template-oss-release-please $\{{ github.ref_name }} $\{{ github.event_name }} + {{ rootNpxPath }} --offline template-oss-release-please "$\{{ github.ref_name }}" "$\{{ inputs.release-pr }}" - name: Post Pull Request Comment if: steps.release.outputs.pr-number uses: actions/github-script@v6 @@ -53,7 +57,7 @@ jobs: body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Update This Release\n\n` body += `This PR will be updated and CI will run for every non-\`chore:\` commit that is pushed to \`{{ defaultBranch }}\`. ` body += `To force CI to update this PR, run this command:\n\n` - body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo}\n\`\`\`` + body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo} -f release-pr=${issue_number}\n\`\`\`` if (commentId) { await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body }) @@ -182,10 +186,14 @@ jobs: with: script: | const { PR_NUMBER: issue_number, RESULT } = process.env - const { repo: { owner, repo } } = context + const { runId, repo: { owner, repo } } = context const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - const updateComment = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith('## Release Workflow\n\n')) + const updateComment = comments.find(c => + c.user.login === 'github-actions[bot]' && + c.body.startsWith('## Release Workflow\n\n') && + c.body.includes(runId) + ) if (updateComment) { console.log('Found comment to update:', JSON.stringify(updateComment, null, 2)) diff --git a/lib/release-please/index.js b/lib/release-please/index.js index 0add36cd..8c8c603b 100644 --- a/lib/release-please/index.js +++ b/lib/release-please/index.js @@ -20,7 +20,18 @@ const omit = (obj, ...keys) => { return res } -const getReleaseArtifacts = async ({ dryRun, github, baseBranch }) => { +const getManifest = async ({ repo: fullRepo, token, branch }) => { + const fullRepoParts = fullRepo.split('/') + const github = await RP.GitHub.create({ + owner: fullRepoParts[0], + repo: fullRepoParts[1], + token, + }) + + const { octokit, repository: { owner, repo, defaultBranch } } = github + + const baseBranch = branch ?? defaultBranch + // This is mostly for testing and debugging. Use environs with the // format `RELEASE_PLEASE_` (eg // `RELEASE_PLEASE_lastReleaseSha=`) to set one-off config items @@ -37,10 +48,56 @@ const getReleaseArtifacts = async ({ dryRun, github, baseBranch }) => { Object.fromEntries(manifestOverrides) ) - let pullRequests - let releases + return { + github, + manifest, + octokit, + owner, + repo, + baseBranch, + } +} + +const getReleasesFromPr = async ({ manifest, github, number }) => { + const baseUrl = `https://github.com/${github.repository.owner}/${github.repository.repo}` + // get the release please formatted pull request + let pullRequest + const prGenerator = github.pullRequestIterator(this.targetBranch, 'MERGED', 200, false) + for await (const pr of prGenerator) { + if (pr.number === number) { + pullRequest = pr + break + } + } + const strategiesByPath = await manifest.getStrategiesByPath() + const releases = [] + for (const path in manifest.repositoryConfig) { + const config = manifest.repositoryConfig[path] + const release = await strategiesByPath[path].buildRelease(pullRequest) + if (release) { + const { tag, ...rest } = release + releases.push({ + ...rest, + ...tag.version, + tagName: tag.toString(), + version: tag.version.toString(), + path, + draft: false, + url: `${baseUrl}/releases/tag/${tag.toString()}`, + prerelease: config.prerelease && !!tag.version.preRelease, + }) + } + } + return releases +} + +const getReleaseArtifacts = async ({ dryRun, manifest, forceReleases }) => { + let pullRequests = [] + let releases = [] - if (dryRun) { + if (forceReleases) { + releases = forceReleases + } else if (dryRun) { pullRequests = await manifest.buildPullRequests() releases = await manifest.buildReleases() } else { @@ -54,26 +111,15 @@ const getReleaseArtifacts = async ({ dryRun, github, baseBranch }) => { } } -const forcePullRequest = async ({ octokit, owner, repo, baseBranch }) => { - const { data: releasePrs } = await octokit.pulls.list({ - owner, - repo, - head: `release-please--branches--${baseBranch}`, - }) - - if (releasePrs.length !== 1) { - throw new Error(`Found ${releasePrs.length} matching PRs, expected 1`) - } - - const [releasePr] = releasePrs +// XXX(hack): to get release please to recreate a pull request it needs +// to have a different body string so we append a message a message that CI +// is running. This will force release-please to rebase the PR but it +// wont update the body again, so we only append to it. +const touchPullRequest = async ({ octokit, owner, repo, releasePr }) => { const id = process.env.GITHUB_RUN_ID ? `by https://github.com/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}` : `manually starting at ${new Date().toJSON()}` - // XXX(hack): to get release please to recreate a pull request it needs - // to have a different body string so we append a message a message that CI - // is running. This will force release-please to rebase the PR but it - // wont update the body again, so we only append to it. await octokit.pulls.update({ owner, repo, @@ -82,33 +128,51 @@ const forcePullRequest = async ({ octokit, owner, repo, baseBranch }) => { }) } -const main = async ({ repo: _fullRepo, token, dryRun, branch, force }) => { +const main = async ({ repo: fullRepo, token, dryRun, branch, forcePullRequest }) => { if (!token) { throw new Error('Token is required') } - if (!_fullRepo) { + if (!fullRepo) { throw new Error('Repo is required') } - const fullRepo = _fullRepo.split('/') - const github = await RP.GitHub.create({ - owner: fullRepo[0], - repo: fullRepo[1], - token, - }) const { + github, octokit, - repository: { owner, repo, defaultBranch }, - } = github + manifest, + owner, + repo, + baseBranch, + } = await getManifest({ repo: fullRepo, token, branch }) - const baseBranch = branch ?? defaultBranch + let forceReleases = null + + if (forcePullRequest) { + const { data: releasePr } = await octokit.rest.pulls.get({ + owner, + repo, + pull_number: forcePullRequest, + }) - if (force) { - await forcePullRequest({ octokit, owner, repo, baseBranch }) + if (!releasePr) { + throw new Error(`Could not find PR from number: ${forcePullRequest}`) + } + + if (releasePr.state === 'open') { + await touchPullRequest({ octokit, owner, repo, releasePr }) + } else if (releasePr.state === 'closed' && releasePr.merged) { + forceReleases = await getReleasesFromPr({ manifest, github, number: releasePr.number }) + } else { + throw new Error(`Could not run workflow on PR with wrong state: ${JSON.stringify( + releasePr, + null, + 2 + )}`) + } } - const { pullRequests, releases } = await getReleaseArtifacts({ dryRun, github, baseBranch }) + const { pullRequests, releases } = await getReleaseArtifacts({ dryRun, manifest, forceReleases }) // We only ever get a single pull request with our current release-please settings // Update this if we start creating individual PRs per workspace release diff --git a/tap-snapshots/test/apply/source-snapshots.js.test.cjs b/tap-snapshots/test/apply/source-snapshots.js.test.cjs index efc77521..a57e40ac 100644 --- a/tap-snapshots/test/apply/source-snapshots.js.test.cjs +++ b/tap-snapshots/test/apply/source-snapshots.js.test.cjs @@ -758,6 +758,10 @@ name: Release on: workflow_dispatch: + inputs: + release-pr: + description: a release PR number to rerun release jobs on + type: string push: branches: - main @@ -807,7 +811,7 @@ jobs: env: GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} run: | - npx --offline template-oss-release-please \${{ github.ref_name }} \${{ github.event_name }} + npx --offline template-oss-release-please "\${{ github.ref_name }}" "\${{ inputs.release-pr }}" - name: Post Pull Request Comment if: steps.release.outputs.pr-number uses: actions/github-script@v6 @@ -830,7 +834,7 @@ jobs: body += \`Release workflow run: \${workflow.html_url}/n/n#### Force CI to Update This Release/n/n\` body += \`This PR will be updated and CI will run for every non-/\`chore:/\` commit that is pushed to /\`main/\`. \` body += \`To force CI to update this PR, run this command:/n/n\` - body += \`/\`/\`/\`/ngh workflow run release.yml -r \${REF_NAME} -R \${owner}/\${repo}/n/\`/\`/\`\` + body += \`/\`/\`/\`/ngh workflow run release.yml -r \${REF_NAME} -R \${owner}/\${repo} -f release-pr=\${issue_number}/n/\`/\`/\`\` if (commentId) { await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body }) @@ -1132,10 +1136,14 @@ jobs: with: script: | const { PR_NUMBER: issue_number, RESULT } = process.env - const { repo: { owner, repo } } = context + const { runId, repo: { owner, repo } } = context const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - const updateComment = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith('## Release Workflow/n/n')) + const updateComment = comments.find(c => + c.user.login === 'github-actions[bot]' && + c.body.startsWith('## Release Workflow/n/n') && + c.body.includes(runId) + ) if (updateComment) { console.log('Found comment to update:', JSON.stringify(updateComment, null, 2)) @@ -2332,6 +2340,10 @@ name: Release on: workflow_dispatch: + inputs: + release-pr: + description: a release PR number to rerun release jobs on + type: string push: branches: - main @@ -2381,7 +2393,7 @@ jobs: env: GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} run: | - npx --offline template-oss-release-please \${{ github.ref_name }} \${{ github.event_name }} + npx --offline template-oss-release-please "\${{ github.ref_name }}" "\${{ inputs.release-pr }}" - name: Post Pull Request Comment if: steps.release.outputs.pr-number uses: actions/github-script@v6 @@ -2404,7 +2416,7 @@ jobs: body += \`Release workflow run: \${workflow.html_url}/n/n#### Force CI to Update This Release/n/n\` body += \`This PR will be updated and CI will run for every non-/\`chore:/\` commit that is pushed to /\`main/\`. \` body += \`To force CI to update this PR, run this command:/n/n\` - body += \`/\`/\`/\`/ngh workflow run release.yml -r \${REF_NAME} -R \${owner}/\${repo}/n/\`/\`/\`\` + body += \`/\`/\`/\`/ngh workflow run release.yml -r \${REF_NAME} -R \${owner}/\${repo} -f release-pr=\${issue_number}/n/\`/\`/\`\` if (commentId) { await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body }) @@ -2706,10 +2718,14 @@ jobs: with: script: | const { PR_NUMBER: issue_number, RESULT } = process.env - const { repo: { owner, repo } } = context + const { runId, repo: { owner, repo } } = context const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - const updateComment = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith('## Release Workflow/n/n')) + const updateComment = comments.find(c => + c.user.login === 'github-actions[bot]' && + c.body.startsWith('## Release Workflow/n/n') && + c.body.includes(runId) + ) if (updateComment) { console.log('Found comment to update:', JSON.stringify(updateComment, null, 2)) @@ -3749,6 +3765,10 @@ name: Release on: workflow_dispatch: + inputs: + release-pr: + description: a release PR number to rerun release jobs on + type: string push: branches: - main @@ -3798,7 +3818,7 @@ jobs: env: GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} run: | - npx --offline template-oss-release-please \${{ github.ref_name }} \${{ github.event_name }} + npx --offline template-oss-release-please "\${{ github.ref_name }}" "\${{ inputs.release-pr }}" - name: Post Pull Request Comment if: steps.release.outputs.pr-number uses: actions/github-script@v6 @@ -3821,7 +3841,7 @@ jobs: body += \`Release workflow run: \${workflow.html_url}/n/n#### Force CI to Update This Release/n/n\` body += \`This PR will be updated and CI will run for every non-/\`chore:/\` commit that is pushed to /\`main/\`. \` body += \`To force CI to update this PR, run this command:/n/n\` - body += \`/\`/\`/\`/ngh workflow run release.yml -r \${REF_NAME} -R \${owner}/\${repo}/n/\`/\`/\`\` + body += \`/\`/\`/\`/ngh workflow run release.yml -r \${REF_NAME} -R \${owner}/\${repo} -f release-pr=\${issue_number}/n/\`/\`/\`\` if (commentId) { await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body }) @@ -4123,10 +4143,14 @@ jobs: with: script: | const { PR_NUMBER: issue_number, RESULT } = process.env - const { repo: { owner, repo } } = context + const { runId, repo: { owner, repo } } = context const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) - const updateComment = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith('## Release Workflow/n/n')) + const updateComment = comments.find(c => + c.user.login === 'github-actions[bot]' && + c.body.startsWith('## Release Workflow/n/n') && + c.body.includes(runId) + ) if (updateComment) { console.log('Found comment to update:', JSON.stringify(updateComment, null, 2))