fix: only render readme if tf-docs.yml is present; move tf-docs step #45
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Monorepo CI | |
concurrency: #avoid concurrent runs on label events, might cause issues on super fast commits ¯\_(ツ)_/¯ | |
group: ${{ github.head_ref }} | |
cancel-in-progress: true | |
on: | |
pull_request: | |
types: [opened, closed, synchronize, reopened, labeled, unlabeled] | |
permissions: | |
pull-requests: read | |
jobs: | |
detect: | |
runs-on: ubuntu-latest | |
name: 'Detect pull request context' | |
permissions: | |
pull-requests: write | |
contents: read | |
repository-projects: read | |
outputs: | |
directories: ${{ steps.condense.outputs.result }} | |
release-type: ${{ steps.check_pr_label.outputs.release-type}} | |
is-merge-event: >- | |
${{ github.event_name == 'pull_request' | |
&& github.event.action == 'closed' | |
&& github.event.pull_request.merged == true }} | |
steps: | |
- uses: actions/checkout@v4 | |
with: | |
ref: ${{ github.event.pull_request.head.ref }} | |
# I'm getting the labels from the API and not the context("contains(github.event.pull_request.labels.*.name, 'Env Promote')") as the labels | |
# are added in 2nd API call so they aren't included in the PR context | |
- name: Check PR labels for semver | |
id: check_pr_label | |
env: | |
PR_URL: ${{github.event.pull_request.html_url}} | |
PR_ID: ${{ github.event.pull_request.number }} | |
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} | |
run: | | |
LABELS=$(gh pr view $PR_URL --json labels --jq '.labels[] | select((.name=="minor") or (.name=="major") or (.name=="patch") or (.name=="no-release")) |.name') | |
NUMBER_OF_LABELS=$(echo "$LABELS" | wc -w) | |
if [ "$NUMBER_OF_LABELS" -eq "1" ] ; then | |
echo "Found: $LABELS" | |
echo "release-type=$LABELS" >> "$GITHUB_OUTPUT" | |
elif [ "$NUMBER_OF_LABELS" -gt "1" ] ; then | |
echo "::error ::Too many release type labels: $( echo $LABELS | tr '\n' ' ' )" | |
exit 1 | |
else | |
echo "::warn ::No release type labels found(patch/minor/major/no-release)" | |
echo "Checking commit messages for semantic labels" | |
# fetch all commits | |
PR_COMMITS=`gh pr view $PR_ID --json commits | jq '.commits | length'` | |
git fetch --no-tags --prune --progress --no-recurse-submodules --deepen=$PR_COMMITS | |
# retrive the commit messages of the PR | |
COMMIT_MESSAGES="$(gh pr view $prId --json commits --jq '.[].[].messageHeadline')" | |
# Check all commits in PR for conventional commit messages - add appropriate label if found | |
# Follows https://www.conventionalcommits.org/en/v1.0.0/ | |
# If there's conflicting messages take the largest semver change | |
if echo "$COMMIT_MESSAGES" | grep --quiet --ignore-case 'breaking change:'; then | |
LABEL="major" | |
elif echo "$COMMIT_MESSAGES" | grep --quiet --ignore-case 'feat:' ; then | |
LABEL="minor" | |
elif echo "$COMMIT_MESSAGES" | grep --quiet --ignore-case 'fix:' ; then | |
LABEL="patch" | |
else | |
echo "::error No release label found, and no conventional commit messages found. Label your PR with major/minor/patch" | |
exit 2 | |
fi | |
echo "release-type=$LABEL" >> "$GITHUB_OUTPUT" | |
gh pr edit $PR_ID --add-label "$LABEL" | |
fi | |
- name: Get changed files | |
uses: tj-actions/changed-files@v45 | |
id: raw-files | |
with: | |
json: "true" | |
escape_json: "false" | |
- name: Condense to directory list | |
uses: actions/github-script@v7 | |
id: condense | |
env: | |
RAW_FILES: ${{ steps.raw-files.outputs.all_changed_files }} | |
with: | |
script: | | |
const raw = JSON.parse(process.env.RAW_FILES); | |
const directories = Array.from(new Set(raw | |
.filter(x => !x.startsWith('.')) | |
.filter(x => x.includes('/')) | |
.map(x => x.split('/')[0]) | |
)); | |
if (directories.length < 1) return {}; | |
return { | |
include: directories.map(directory => ({ directory })), | |
}; | |
plan: | |
needs: detect | |
name: 'Release plan & docs for module: ${{ matrix.directory }}' | |
if: needs.detect.outputs.directories != '{}' && needs.detect.outputs.release-type != 'no-release' | |
runs-on: ubuntu-latest | |
outputs: | |
new-version: ${{ steps.new-version.outputs.result }} | |
permissions: | |
contents: write | |
pull-requests: read | |
strategy: | |
matrix: "${{ fromJson(needs.detect.outputs.directories) }}" | |
fail-fast: false | |
# Do serially so git operations don't collide | |
max-parallel: 1 | |
steps: | |
- name: Extract changelog entry | |
uses: actions/github-script@v7 | |
id: changelog | |
with: | |
script: | | |
const { owner, repo } = context.repo; | |
const { data: prInfo } = await github.rest.pulls.get({ | |
owner, repo, | |
pull_number: context.issue.number, | |
}); | |
console.log('Found PR body:|'); | |
console.log(prInfo.body); | |
const changelogEntry = ((prInfo.body | |
.split(/^#+ ?/m) | |
.find(x => x.startsWith('Changelog')) | |
|| '').split(/^```/m)[1] || '').trim(); | |
if (!changelogEntry) | |
throw `'Changelog' section not found in PR body! Please add it back.`; | |
if (changelogEntry.match(/^TODO:/m)) | |
throw `'Changelog' section needs proper text, instead of 'TODO'`; | |
const { writeFile } = require('fs').promises; | |
const entry=`* PR [#${ prInfo.number }](${ prInfo.html_url }) - ${ prInfo.title } | |
\`\`\` | |
${changelogEntry} | |
\`\`\` | |
` | |
return entry | |
- name: Checkout repository to use composite action | |
uses: actions/checkout@v4 | |
with: | |
ref: main # Only use composite action from main to prevent malicious PRs | |
# Do the per-module steps in a composite action because matrixes can't handle dynamic outputs | |
- name: Generate docs and version bump | |
uses: mozilla/terraform-modules/.github/actions@main | |
with: | |
package-name: ${{ matrix.directory }} | |
changelog-entry: ${{ steps.changelog.outputs.result }} | |
release-type: ${{ needs.detect.outputs.release-type }} | |
comment: | |
needs: [detect, plan] | |
if: needs.detect.outputs.is-merge-event == 'false' && needs.detect.outputs.release-type != 'no-release' | |
runs-on: ubuntu-latest | |
name: 'Comment on PR with release plan' | |
permissions: | |
issues: write | |
pull-requests: write | |
contents: write | |
steps: | |
- uses: actions/[email protected] | |
with: | |
path: outputs | |
- name: Display structure of downloaded files | |
run: ls -R | |
working-directory: outputs | |
- uses: actions/github-script@v7 | |
id: comment | |
with: | |
script: | | |
const { owner, repo } = context.repo; | |
const { number: issue_number } = context.issue; | |
const { readdir, readFile } = require('fs').promises; | |
const utf8 = { encoding: 'utf-8' }; | |
const lines = [ | |
'# Release plan', '', | |
'| Directory | Previous version | New version |', | |
'|--|--|--|', | |
]; | |
const sections = []; | |
for (const folder of await readdir('outputs', { withFileTypes: true })) { | |
if (!folder.isDirectory()) continue; | |
const readText = (name) => readFile(name, utf8).then(x => x.trim()); | |
lines.push('| '+[ | |
`\`${folder.name}\``, | |
`${await readText(`outputs/${folder.name}/previous-version.txt`)}`, | |
`**${await readText(`outputs/${folder.name}/new-version.txt`)}**`, | |
].join(' | ')+' |'); | |
} | |
const finalBody = [lines.join('\n'), ...sections].join('\n\n'); | |
const {data: allComments} = await github.rest.issues.listComments({ issue_number, owner, repo }); | |
const ourComments = allComments | |
.filter(comment => comment.user.login === 'github-actions[bot]') | |
.filter(comment => comment.body.startsWith(lines[0]+'\n')); | |
const latestComment = ourComments.slice(-1)[0]; | |
if (latestComment && latestComment.body === finalBody) { | |
console.log('Existing comment is already up to date.'); | |
return; | |
} | |
const {data: newComment} = await github.rest.issues.createComment({ issue_number, owner, repo, body: finalBody }); | |
console.log('Posted comment', newComment.id, '@', newComment.html_url); | |
// Delete all our previous comments | |
for (const comment of ourComments) { | |
if (comment.id === newComment.id) continue; | |
console.log('Deleting previous PR comment from', comment.created_at); | |
await github.rest.issues.deleteComment({ comment_id: comment.id, owner, repo }); | |
} | |
trigger-release: | |
needs: [plan] | |
if: needs.detect.outputs.is-merge-event == 'true' && needs.detect.outputs.release-type != 'no-release' | |
runs-on: ubuntu-latest | |
name: 'Dispatch release event' | |
permissions: | |
actions: write | |
contents: write | |
steps: | |
- uses: actions/[email protected] | |
with: | |
path: outputs | |
- name: Combine version information | |
id: extract-releases | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const { readdir, readFile } = require('fs').promises; | |
const utf8 = { encoding: 'utf-8' }; | |
const readText = (name) => readFile(name, utf8).then(x => x.trim()); | |
const directories = await readdir('outputs', { withFileTypes: true }); | |
return await Promise.all(directories | |
.filter(x => x.isDirectory()) | |
.map(async folder => ({ | |
module: folder.name, | |
prevVersion: await readText(`outputs/${folder.name}/previous-version.txt`), | |
newVersion: await readText(`outputs/${folder.name}/new-version.txt`), | |
}))); | |
- name: Dispatch monorepo_release event | |
uses: actions/github-script@v7 | |
env: | |
RELEASE_LIST: ${{ steps.extract-releases.outputs.result }} | |
with: | |
script: | | |
const payload = { | |
run_id: "${{ github.run_id }}", | |
sha: context.sha, | |
releases: JSON.parse(process.env.RELEASE_LIST), | |
}; | |
console.log('Event payload:', JSON.stringify(payload, null, 2)); | |
const { owner, repo } = context.repo; | |
await github.rest.repos.createDispatchEvent({ | |
owner, repo, | |
event_type: 'monorepo_release', | |
client_payload: payload, | |
}); |