Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- Describe your Pull Request here, as normal :) -->

## Changelog entry
```
TODO: Replace this inner text with a useful message
for users of the affected modules!
```
33 changes: 33 additions & 0 deletions .github/workflows/monorepo-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: 'Monorepo: Release'
on:
repository_dispatch:
types: [monorepo_release]

jobs:

tag:
runs-on: ubuntu-latest
name: 'Tag Releases'
permissions:
contents: write
steps:
- uses: actions/github-script@v7
env:
CLIENT_PAYLOAD: ${{ toJSON(github.event.client_payload) }}
with:
script: |
const { sha, releases } = JSON.parse(process.env.CLIENT_PAYLOAD);

for (const release of releases) {
const tagName = `${release.module}-${release.newVersion}`;

const ref = `refs/tags/${tagName}`;
console.log('Tagging', tagName, 'as', sha);
console.log('Owner', context.repo.owner, 'repo', context.repo.repo);
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: ref,
sha: sha
});
}
331 changes: 331 additions & 0 deletions .github/workflows/monorepo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
name: Monorepo Versioning
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, synchronize, reopened, labeled, unlabeled]
pull_request_target:
types: [closed]

permissions:
pull-requests: read

jobs:
detect:
runs-on: ubuntu-latest
name: 'Detect pull request context'
outputs:
directories: ${{ steps.condense.outputs.result }}
release-type: ${{ steps.check_pr_label.outputs.release-type}}
is-merge-event: >-
${{ github.event_name == 'pull_request_target'
&& github.event.action == 'closed'
&& github.event.pull_request.merged == true }}
steps:
# 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
id: check_pr_label
env:
PR_URL: ${{github.event.pull_request.html_url}}
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 "::error ::No release type labels found(patch/minor/major/no-release)"
exit 2
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
if: ${{ needs.detect.outputs.directories != '{}' }}
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
strategy:
matrix: "${{ fromJson(needs.detect.outputs.directories) }}"
fail-fast: false
name: 'Module: ${{ matrix.directory }}'
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
fetch-depth: 0
- name: Detect previous version number
id: prev-version
env:
PACKAGE_NAME: '${{ matrix.directory }}'
run: |
git fetch --tags
TAG=$(git for-each-ref --sort=-creatordate --count 1 --format="%(refname:short)" "refs/tags/$PACKAGE_NAME-[0-9].[0-9].[0-9]")

if [ -z "$TAG" ] ; then
echo "No git tag found for $PACKAGE_NAME, using 0.0.0 as previous version"
echo "result=0.0.0" >> "$GITHUB_OUTPUT"
exit 0
fi

TAG_VERSION="${TAG#*-}"
echo "TAG_VERSION = $TAG_VERSION"
SEMVER_REGEX="^[0-9].[0-9].[0-9]$"
if [[ $TAG_VERSION =~ $SEMVER_REGEX ]] ; then
echo "$TAG is valid semver, using it"
echo "result=${TAG_VERSION}" >> "$GITHUB_OUTPUT"
exit 0
else
echo "Error: $TAG does not end in a valid semver"
exit 1
fi

- name: Determine new version number
uses: actions/github-script@v7
id: new-version
env:
PREV_VERSION: ${{ steps.prev-version.outputs.result }}
RELEASE_TYPE: ${{ needs.detect.outputs.release-type }}
with:
script: |
const { PREV_VERSION, RELEASE_TYPE } = process.env;
console.log('Previous version was', PREV_VERSION);
console.log('Release type is', RELEASE_TYPE);

const numbers = PREV_VERSION.split('.');
const numberIdx = ['major', 'minor', 'patch'].indexOf(RELEASE_TYPE);
numbers[numberIdx] = parseInt(numbers[numberIdx]) + 1;
for (let i = numberIdx + 1; i < numbers.length; i++) {
numbers[i] = 0;
}
return numbers.join('.');
result-encoding: string

- name: Store version numbers
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: |
mkdir output
echo '${{ steps.prev-version.outputs.result }}' > output/previous-version.txt
echo '${{ steps.new-version.outputs.result }}' > output/new-version.txt

- name: Extract changelog entry
uses: actions/github-script@v7
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;
await writeFile('output/changelog.md', `
* PR [#${ prInfo.number }](${ prInfo.html_url }) - ${ prInfo.title }

\`\`\`
${changelogEntry}
\`\`\`
`.trimLeft(), { encoding: 'utf-8' })

- name: Document example 'source' line
run: |
cat > output/documentation.md <<'EOF'
# Module Location
To use this module in your Terraform, use the below source value (substitute in a real version for the X's).

```hcl
module "my_${{ matrix.directory }}" {
source = "git@github.com:mozilla/terraform-modules.git//${{ matrix.directory }}?ref=${{ matrix.directory }}-X.X.X"
# also any inputs for the module (see below)
}
```

EOF

- name: Install terraform docs
run: |
wget https://github.com/terraform-docs/terraform-docs/releases/download/v0.18.0/terraform-docs-v0.18.0-linux-amd64.tar.gz \
--output-document - \
--progress dot:mega \
| tar -xvz
- name: Clone repository for analysis
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
path: src
- name: Render terraform docs
run: |
echo '# Module Attributes' >> output/documentation.md
./terraform-docs markdown table \
--output-file "$(pwd)"/output/documentation.md \
--sort-by required \
'src/${{ matrix.directory }}'
- name: Upload result artifacts
uses: actions/upload-artifact@v4
with:
name: '${{ matrix.directory }}'
path: output
retention-days: 5


comment:
needs: [detect, plan]
if: needs.detect.outputs.is-merge-event == 'false'
runs-on: ubuntu-latest
name: 'Comment on PR'
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/download-artifact@v4.1.8
with:
path: outputs
- name: Display structure of downloaded files
run: ls -R
working-directory: outputs

- uses: actions/github-script@v7
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(' | ')+' |');

sections.push(`<details><summary>Changelog preview: ${folder.name}</summary>\n\n${await readText(`outputs/${folder.name}/changelog.md`)}\n</details>`);
}

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: [detect, plan]
if: needs.detect.outputs.is-merge-event == 'true'
runs-on: ubuntu-latest
name: 'Dispatch release event'
permissions:
actions: write
contents: write
steps:
- uses: actions/download-artifact@v4.1.8
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,
});
23 changes: 0 additions & 23 deletions .github/workflows/release.yaml

This file was deleted.

Loading