diff --git a/changelog/submit/apply/README.md b/changelog/submit/apply/README.md index d3738db5..e5f71a56 100755 --- a/changelog/submit/apply/README.md +++ b/changelog/submit/apply/README.md @@ -1,15 +1,14 @@ # Changelog submit / apply -Write action that downloads the changelog artifact produced by the evaluate action and either commits it to the PR branch or posts it as a PR comment. Requires contents:write and pull-requests:write permissions. +Write action that downloads the changelog artifact produced by the evaluate action, commits the changelog entry to the PR branch, and uploads a changelog-decision artifact for the downstream comment workflow. Requires contents:write and pull-requests:write permissions. ## Inputs -| Name | Description | Required | Default | -|----------------|--------------------------------------------------------------------|----------|-----------------------| -| `github-token` | GitHub token with contents:write and pull-requests:write | `false` | `${{ github.token }}` | -| `config` | Path to changelog.yml configuration file (used in failure comment) | `false` | `docs/changelog.yml` | +| Name | Description | Required | Default | +|----------------|-----------------------------------------------------------|----------|-----------------------| +| `github-token` | GitHub token with contents:write and pull-requests:write | `false` | `${{ github.token }}` | ## Outputs diff --git a/changelog/submit/apply/action.yml b/changelog/submit/apply/action.yml index a9b1c378..6af515b2 100644 --- a/changelog/submit/apply/action.yml +++ b/changelog/submit/apply/action.yml @@ -1,16 +1,14 @@ name: Changelog submit / apply description: > Write action that downloads the changelog artifact produced by the evaluate - action and either commits it to the PR branch or posts it as a PR comment. + action, commits the changelog entry to the PR branch, and uploads a + changelog-decision artifact for the downstream comment workflow. Requires contents:write and pull-requests:write permissions. inputs: github-token: description: 'GitHub token with contents:write and pull-requests:write' default: '${{ github.token }}' - config: - description: 'Path to changelog.yml configuration file (used in failure comment)' - default: 'docs/changelog.yml' outputs: committed: @@ -131,51 +129,36 @@ runs: run: | echo "::warning::Changelog commit step failed; posting the entry as a PR comment only. Same-repository PR entries are not regenerated by the merge-time upload, so resolve the commit failure before merging." - - name: Post success comment - if: steps.commit.outputs.committed == 'true' - uses: actions/github-script@v9 - env: - PR_NUMBER: ${{ steps.meta.outputs.pr-number }} - HEAD_REF: ${{ steps.meta.outputs.head-ref }} - CHANGELOG_FILE: ${{ steps.commit.outputs.changelog-file }} - with: - github-token: ${{ inputs.github-token }} - script: | - const script = require('${{ github.action_path }}/scripts/post-success-comment.js'); - await script({ github, context, core }); - - # Two paths arrive here: - # 1. The strategy chose comment-only (fork PR from a trusted author, or - # explicit `comment-only: true` input). - # 2. The strategy chose commit but the commit step failed; rather than - # hard-fail, expose the generated entry on the PR so reviewers and the - # author can still see what would have been recorded. - - name: Post comment-only changelog - if: steps.meta.outputs.should-comment-success == 'true' || steps.commit.outcome == 'failure' - uses: actions/github-script@v9 + - name: Record decision and stage comment artifact + if: always() + continue-on-error: true + shell: bash env: - PR_NUMBER: ${{ steps.meta.outputs.pr-number }} - CHANGELOG_DIR: ${{ steps.meta.outputs.changelog-dir }} - STAGING_DIR: /tmp/changelog-staging - IS_FORK: ${{ steps.meta.outputs.is-fork }} - COMMIT_FAILED: ${{ steps.commit.outcome == 'failure' }} - with: - github-token: ${{ inputs.github-token }} - script: | - const script = require('${{ github.action_path }}/scripts/post-comment-only.js'); - await script({ github, context, core }); + COMMIT_OUTCOME: ${{ steps.commit.outputs.committed == 'true' && 'committed' || steps.commit.outcome == 'failure' && 'failed' || 'none' }} + COMMITTED_FILE: ${{ steps.commit.outputs.changelog-file }} + run: | + mkdir -p .artifacts/changelog-decision + cp /tmp/changelog-staging/metadata.json .artifacts/changelog-decision/metadata.json - - name: Post failure comment - if: steps.meta.outputs.should-comment-failure == 'true' - uses: actions/github-script@v9 - env: - PR_NUMBER: ${{ steps.meta.outputs.pr-number }} - LABEL_TABLE: ${{ steps.meta.outputs.label-table }} - PRODUCT_LABEL_TABLE: ${{ steps.meta.outputs.product-label-table }} - SKIP_LABELS: ${{ steps.meta.outputs.skip-labels }} - CONFIG_FILE: ${{ inputs.config }} + # Copy the generated YAML so the comment renderer can show it in comment-only mode + GENERATED=$(ls /tmp/changelog-staging/*.yaml 2>/dev/null | head -1) + if [ -n "$GENERATED" ]; then + cp "$GENERATED" .artifacts/changelog-decision/ + fi + + ARGS=(--metadata .artifacts/changelog-decision/metadata.json --commit-outcome "$COMMIT_OUTCOME") + if [ -n "$COMMITTED_FILE" ]; then + ARGS+=(--committed-file "$COMMITTED_FILE") + fi + + docs-builder changelog github-decision "${ARGS[@]}" + + - name: Upload decision artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@v7 with: - github-token: ${{ inputs.github-token }} - script: | - const script = require('${{ github.action_path }}/scripts/post-failure-comment.js'); - await script({ github, context, core }); + name: changelog-decision + path: .artifacts/changelog-decision + retention-days: 1 + if-no-files-found: ignore diff --git a/changelog/submit/apply/scripts/comment-helper.js b/changelog/submit/apply/scripts/comment-helper.js deleted file mode 100644 index 94e83632..00000000 --- a/changelog/submit/apply/scripts/comment-helper.js +++ /dev/null @@ -1,36 +0,0 @@ -const TITLE = '### šŸ“‹ Changelog'; - -const longestBacktickRun = (value) => { - const runs = String(value ?? '').match(/`+/g) ?? []; - return runs.reduce((longest, run) => Math.max(longest, run.length), 0); -}; - -const wrapCodeFence = (content, language = '') => { - const text = String(content ?? ''); - const fence = '`'.repeat(Math.max(3, longestBacktickRun(text) + 1)); - return `${fence}${language}\n${text}\n${fence}`; -}; - -const wrapInlineCode = (value) => { - const text = String(value ?? ''); - const delimiter = '`'.repeat(longestBacktickRun(text) + 1); - const padded = text.startsWith('`') || text.endsWith('`') ? ` ${text} ` : text; - return `${delimiter}${padded}${delimiter}`; -}; - -async function upsertComment({ github, context, prNumber, body }) { - const { owner, repo } = context.repo; - const comments = await github.paginate(github.rest.issues.listComments, { - owner, repo, issue_number: prNumber, per_page: 100, - }); - const existing = comments.find(c => - c.user?.login === 'github-actions[bot]' && c.body?.startsWith(TITLE) - ); - if (existing) { - await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); - } else { - await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body }); - } -} - -module.exports = { TITLE, upsertComment, wrapCodeFence, wrapInlineCode }; diff --git a/changelog/submit/apply/scripts/post-comment-only.js b/changelog/submit/apply/scripts/post-comment-only.js deleted file mode 100644 index d6661b11..00000000 --- a/changelog/submit/apply/scripts/post-comment-only.js +++ /dev/null @@ -1,39 +0,0 @@ -const fs = require('fs'); -const { TITLE, upsertComment, wrapCodeFence, wrapInlineCode } = require('./comment-helper'); - -module.exports = async ({ github, context, core }) => { - const prNumber = parseInt(process.env.PR_NUMBER, 10); - const changelogDir = process.env.CHANGELOG_DIR; - const stagingDir = process.env.STAGING_DIR || '/tmp/changelog-staging'; - const isFork = process.env.IS_FORK === 'true'; - const commitFailed = process.env.COMMIT_FAILED === 'true'; - - const files = fs.readdirSync(stagingDir).filter(f => f.endsWith('.yaml')); - const content = files.length > 0 - ? fs.readFileSync(`${stagingDir}/${files[0]}`, 'utf8').trim() - : ''; - - const bodyParts = [TITLE, '']; - if (content) { - let guidance; - if (commitFailed) { - guidance = 'The workflow could not commit this generated changelog change to the PR branch, so the merge-time S3 upload will not include it. Resolve the commit failure or add the generated entry to the PR branch before merging.'; - } else if (isFork) { - guidance = 'This comment is informational — editing it does not change what gets uploaded. On merge, the entry is regenerated from the live PR record (title, labels) and uploaded to S3. To change the preview, edit the PR title or labels and let the changelog workflow re-run.'; - } else { - guidance = 'This comment is informational — editing it does not change what gets uploaded. Comment-only mode did not commit this generated changelog change to the PR branch, and same-repository PRs are not regenerated by the merge-time S3 upload. Add the generated entry to the PR branch before merging if it should be published.'; - } - - bodyParts.push( - `Generated changelog entry for ${wrapInlineCode(changelogDir + '/' + files[0])}:`, - '', - wrapCodeFence(content, 'yaml'), - '', - guidance, - ); - } else { - bodyParts.push('āš ļø Changelog entry was generated but the file content could not be read.'); - } - - await upsertComment({ github, context, prNumber, body: bodyParts.join('\n') }); -}; diff --git a/changelog/submit/apply/scripts/post-failure-comment.js b/changelog/submit/apply/scripts/post-failure-comment.js deleted file mode 100644 index 85b06c13..00000000 --- a/changelog/submit/apply/scripts/post-failure-comment.js +++ /dev/null @@ -1,54 +0,0 @@ -const { TITLE, upsertComment, wrapInlineCode } = require('./comment-helper'); - -module.exports = async ({ github, context, core }) => { - const prNumber = parseInt(process.env.PR_NUMBER, 10); - const configFile = process.env.CONFIG_FILE || 'docs/changelog.yml'; - const labelRows = (process.env.LABEL_TABLE || '').trim(); - const productLabelRows = (process.env.PRODUCT_LABEL_TABLE || '').trim(); - const skipLabels = process.env.SKIP_LABELS || ''; - const configFileCode = wrapInlineCode(configFile); - - const hasTypeIssue = !!labelRows; - const hasProductIssue = !!productLabelRows; - - let headline; - if (hasTypeIssue && hasProductIssue) { - headline = 'āš ļø **Cannot generate changelog:** required type and product labels are missing on this PR.'; - } else if (hasProductIssue) { - headline = 'āš ļø **Cannot generate changelog:** no matching product label found on this PR.'; - } else { - headline = 'āš ļø **Cannot generate changelog:** no matching type label found on this PR.'; - } - - const sections = []; - - if (hasTypeIssue) { - sections.push(['', 'šŸ”– Add one of these **type** labels to your PR:', '', labelRows].join('\n')); - } else if (!hasProductIssue) { - sections.push(`\nAdd a type label that matches your ${wrapInlineCode('pivot.types')} configuration in ${configFileCode}.`); - } - - if (hasProductIssue) { - sections.push(['', 'šŸ“¦ Add one or more **product** labels to your PR:', '', productLabelRows].join('\n')); - } - - let skipSection; - if (skipLabels.trim()) { - const formatted = skipLabels.split(',').map(label => wrapInlineCode(label.trim())).join(', '); - skipSection = `\nā­ļø To skip changelog generation, add one of these labels: ${formatted}`; - } else { - skipSection = `\nā­ļø No skip labels are configured. To allow skipping changelog generation, add a label to ${wrapInlineCode('rules.create.exclude')} in ${configFileCode}.`; - } - - const body = [ - TITLE, - '', - headline, - ...sections, - skipSection, - '', - `šŸ“„ See ${configFileCode} for the full changelog configuration.`, - ].join('\n'); - - await upsertComment({ github, context, prNumber, body }); -}; diff --git a/changelog/submit/apply/scripts/post-success-comment.js b/changelog/submit/apply/scripts/post-success-comment.js deleted file mode 100644 index dfeb6cf7..00000000 --- a/changelog/submit/apply/scripts/post-success-comment.js +++ /dev/null @@ -1,22 +0,0 @@ -const { TITLE, upsertComment, wrapInlineCode } = require('./comment-helper'); - -module.exports = async ({ github, context, core }) => { - const prNumber = parseInt(process.env.PR_NUMBER, 10); - const branch = process.env.HEAD_REF; - const changelogFile = process.env.CHANGELOG_FILE; - const { owner, repo } = context.repo; - const safeBranch = encodeURIComponent(branch); - const safePath = changelogFile.split('/').map(encodeURIComponent).join('/'); - const viewUrl = `https://github.com/${owner}/${repo}/blob/${safeBranch}/${safePath}`; - const editUrl = `https://github.com/${owner}/${repo}/edit/${safeBranch}/${safePath}`; - - const body = [ - TITLE, - '', - `šŸ“ Changelog entry committed: [${wrapInlineCode(changelogFile)}](${viewUrl})`, - '', - `āœļø [Edit this changelog](${editUrl})`, - ].join('\n'); - - await upsertComment({ github, context, prNumber, body }); -};