Chore: update changelog workflow (#90608)

* try integrating

* pass tags

* change section order

* use better terminology

* one more attempt

* keep delimiters

* attempt to patch changelog

* quotes, bash quotes...

* use proper content file

* parens around date

* time for a pr

* first checkout, then create user

* add latest input

* git push

* use square brackets

* formatting

* update release-pr

* fix typo

* try sparse checkout

* fetch depth zero

* clean up after changelog generator
This commit is contained in:
Serge Zaitsev 2024-07-19 18:27:59 +02:00 committed by GitHub
parent 8f54e3bfb7
commit f8b092aba6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 171 additions and 51 deletions

View File

@ -1,13 +1,13 @@
name: Changelog generator
description: Generates and publishes a changelog for the given release version
inputs:
version:
description: Version number
target:
description: Target tag, branch or commit hash for the changelog
required: true
prev_version:
description: Previous version number
previous:
description: Previous tag, branch or commit hash to start changelog from
required: false
token:
github_token:
description: GitHub token with read/write access to all necessary repositories
required: true
output_file:

View File

@ -52,17 +52,20 @@ const getPreviousVersion = async (version) => {
.filter((tag) => tag)
.sort(semverCompare)
.find((tag) => semverCompare(tag, semverParse(version)) > 0);
return prev;
if (!prev) {
throw `Could not find previous git tag for ${version}`;
}
return prev[4];
};
// A helper for Github GraphQL API endpoint
const graphql = async (token, query, variables) => {
const graphql = async (ghtoken, query, variables) => {
const { env } = process;
const results = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
Authorization: `Bearer ${ghtoken}`,
},
body: JSON.stringify({ query, variables }),
});
@ -76,7 +79,7 @@ const graphql = async (token, query, variables) => {
// "commitish" items and get a list of PRs in between them.
const getCommitishDate = async (name, owner, target) => {
const result = await graphql(
token,
ghtoken,
`
query getCommitDate($owner: String!, $name: String!, $target: String!) {
repository(owner: $owner, name: $name) {
@ -96,9 +99,8 @@ const getCommitishDate = async (name, owner, target) => {
// Using Github GraphQL API get a list of PRs between the two "commitish" items.
// This resoves the "since" item's timestamp first and iterates over all PRs
// till "target" using naïve pagination.
const getHistory = async (name, owner, target, since) => {
const sinceDate = await getCommitishDate(name, owner, since);
LOG(`Fetching ${owner}/${name} PRs since ${since} (${sinceDate}) till ${target}`);
const getHistory = async (name, owner, target, sinceDate) => {
LOG(`Fetching ${owner}/${name} PRs since ${sinceDate} till ${target}`);
const query = `
query findCommitsWithAssociatedPullRequests(
$name: String!
@ -150,7 +152,7 @@ const getHistory = async (name, owner, target, since) => {
let cursor;
let nodes = [];
for (;;) {
const result = await graphql(token, query, {
const result = await graphql(ghtoken, query, {
name,
owner,
target,
@ -173,11 +175,11 @@ const getHistory = async (name, owner, target, since) => {
// feature, deprecation, breaking change and plugin fixes/enhancements).
//
// PR grouping relies on Github labels only, not on the PR contents.
const getChangeLogItems = async (name, owner, from, to) => {
const getChangeLogItems = async (name, owner, sinceDate, to) => {
// check if a node contains a certain label
const hasLabel = ({ labels }, label) => labels.nodes.some(({ name }) => name === label);
// get all the PRs between the two "commitish" items
const history = await getHistory(name, owner, to, from);
const history = await getHistory(name, owner, to, sinceDate);
const items = history.flatMap((node) => {
// discard PRs without a "changelog" label
@ -216,20 +218,26 @@ const getChangeLogItems = async (name, owner, from, to) => {
// ======================================================
LOG(`Changelog action started`);
const version = process.argv[2] || process.env.INPUT_VERSION;
LOG(`Target version: ${version}`);
const prevVersion = process.argv[3] || process.env.INPUT_PREV_VERSION || (await getPreviousVersion(version));
LOG(`Previous version: ${prevVersion}`);
const token = process.env.GITHUB_TOKEN || process.env.INPUT_TOKEN;
if (!token) {
throw 'GITHUB_TOKEN is not set and "token" input is empty';
const ghtoken = process.env.GITHUB_TOKEN || process.env.INPUT_GITHUB_TOKEN;
if (!ghtoken) {
throw 'GITHUB_TOKEN is not set and "github_token" input is empty';
}
const target = process.argv[2] || process.env.INPUT_TARGET;
LOG(`Target tag/branch/commit: ${target}`);
const previous = process.argv[3] || process.env.INPUT_PREVIOUS || (await getPreviousVersion(target));
LOG(`Previous tag/commit: ${previous}`);
const sinceDate = await getCommitishDate('grafana', 'grafana', previous);
LOG(`Previous tag/commit timestamp: ${sinceDate}`);
// Get all changelog items from Grafana OSS
const oss = await getChangeLogItems('grafana', 'grafana', prevVersion, version);
const oss = await getChangeLogItems('grafana', 'grafana', sinceDate, target);
// Get all changelog items from Grafana Enterprise
const entr = await getChangeLogItems('grafana-enterprise', 'grafana', prevVersion, version);
const entr = await getChangeLogItems('grafana-enterprise', 'grafana', sinceDate, target);
LOG(`Found OSS PRs: ${oss.length}`);
LOG(`Found Enterprise PRs: ${entr.length}`);
@ -251,13 +259,10 @@ const changelog = [...oss, ...entr]
return changelog;
},
{
version,
breaking: [],
plugins: [],
bugfixes: [],
features: [],
// looks like JS doesn't have a nicer way to format date as YYYY-MM-DD
releaseDate: new Date().toISOString().split('T')[0],
}
);
@ -287,12 +292,10 @@ ${items
`;
// Render all present sections for the given changelog
return `# ${changelog.version} (${changelog.releaseDate})
${section('Breaking changes', changelog.breaking)}
return `${section('Features and enhancements', changelog.features)}
${section('Bug fixes', changelog.bugfixes)}
${section('Breaking changes', changelog.breaking)}
${section('Plugin development fixes & changes', changelog.plugins)}
${section('Features and enhancements', changelog.features)}
`;
};
@ -314,4 +317,3 @@ if (process.env.INPUT_OUTPUT_FILE) {
LOG(`Output to ${process.env.INPUT_OUTPUT_FILE}`);
writeFileSync(process.env.INPUT_OUTPUT_FILE, md);
}

View File

@ -4,10 +4,23 @@ on:
inputs:
version:
required: true
description: 'Target release version (git tag)'
prev_version:
description: 'Target release version (semver, git tag, branch or commit)'
target:
required: true
description: 'Previous release version (git tag)'
type: string
description: 'The base branch that these changes are being merged into'
dry_run:
required: false
default: false
type: bool
latest:
required: false
default: false
type: bool
permissions:
pull-requests: write
jobs:
main:
runs-on: ubuntu-latest
@ -23,21 +36,67 @@ jobs:
- name: "Checkout Grafana repo"
uses: "actions/checkout@v4"
with:
sparse-checkout: .github/workflows
sparse-checkout: |
.github/workflows
CHANGELOG.md
fetch-depth: 0
fetch-tags: true
- name: "Configure git user"
run: |
git config --local user.name "github-actions[bot]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local --add --bool push.autoSetupRemote true
- name: "Create branch"
run: git checkout -b "release/${{ github.run_id }}/${{ inputs.version }}"
- name: "Generate changelog"
id: changelog
uses: ./.github/workflows/actions/changelog
with:
token: ${{ steps.generate_token.outputs.token }}
version: ${{ inputs.version }}
prev_version: ${{ inputs.prev_version }}
output_file: _changelog.md
- name: "Draft Github Release"
github_token: ${{ steps.generate_token.outputs.token }}
target: v${{ inputs.version }}
output_file: changelog_items.md
- name: "Patch CHANGELOG.md"
run: |
# Prepare CHANGELOG.md content with version delimiters
(
echo
echo "# ${{ inputs.version}} ($(date '+%F'))"
echo
cat changelog_items.md
) > CHANGELOG.part
# Check if a version exists in the changelog
if grep -q "<!-- ${{ inputs.version}} START" CHANGELOG.md ; then
# Replace the content between START and END delimiters
echo "Version ${{ inputs.version }} is found in the CHANGELOG.md, patching contents..."
sed -i -e '/${{ inputs.version }} START/,/${{ inputs.version }} END/{//!d;}' \
-e '/${{ inputs.version }} START/r CHANGELOG.part' CHANGELOG.md
else
# Prepend changelog part to the main changelog file
echo "Version ${{ inputs.version }} not found in the CHANGELOG.md"
(
echo "<!-- ${{ inputs.version }} START -->"
cat CHANGELOG.part
echo "<!-- ${{ inputs.version }} END -->"
cat CHANGELOG.md
) > CHANGELOG.tmp
mv CHANGELOG.tmp CHANGELOG.md
fi
git diff CHANGELOG.md
- name: "Commit changelog changes"
run: git commit --allow-empty -m "Update changelog placeholder" CHANGELOG.md
- name: "git push"
if: ${{ inputs.dry_run }} != true
run: git push
- name: "Create changelog PR"
if: "${{ inputs.backport == '' }}"
run: >
gh pr create \
$( [ "x${{ inputs.latest }}" == "xtrue" ] && printf %s '-l "release/latest"') \
--dry-run=${{ inputs.dry_run }} \
-B "${{ inputs.target }}" \
--title "Release: ${{ inputs.version }}" \
--body "Changelog changes for release ${{ inputs.version }}"
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
cat _changelog.md
# skip for now
# gh release create --draft ${{ inputs.version }} --notes-file _changelog.md

View File

@ -30,7 +30,7 @@ on:
type: bool
permissions:
content: write
contents: write
pull-requests: write
jobs:
@ -39,44 +39,103 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'grafana/grafana'
steps:
- name: Generate bot token
id: generate_token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }}
private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }}
- name: Checkout Grafana
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Configure git user
run: |
git config --local user.name "github-actions[bot]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local --add --bool push.autoSetupRemote true
- name: Create branch
run: git checkout -b "release/${{ github.run_id }}/${{ inputs.version }}"
- name: Generate changelog
run: git commit --allow-empty -m "Update changelog placeholder"
id: changelog
uses: ./.github/workflows/actions/changelog
with:
github_token: ${{ steps.generate_token.outputs.token }}
target: v${{ inputs.version }}
output_file: changelog_items.md
- name: Patch CHANGELOG.md
run: |
# Prepare CHANGELOG.md content with version delimiters
(
echo
echo "# ${{ inputs.version}} ($(date '+%F'))"
echo
cat changelog_items.md
) > CHANGELOG.part
# Check if a version exists in the changelog
if grep -q "<!-- ${{ inputs.version}} START" CHANGELOG.md ; then
# Replace the content between START and END delimiters
echo "Version ${{ inputs.version }} is found in the CHANGELOG.md, patching contents..."
sed -i -e '/${{ inputs.version }} START/,/${{ inputs.version }} END/{//!d;}' \
-e '/${{ inputs.version }} START/r CHANGELOG.part' CHANGELOG.md
else
# Prepend changelog part to the main changelog file
echo "Version ${{ inputs.version }} not found in the CHANGELOG.md"
(
echo "<!-- ${{ inputs.version }} START -->"
cat CHANGELOG.part
echo "<!-- ${{ inputs.version }} END -->"
cat CHANGELOG.md
) > CHANGELOG.tmp
mv CHANGELOG.tmp CHANGELOG.md
fi
rm -f CHANGELOG.part changelog_items.md
git diff CHANGELOG.md
- name: Commit CHANGELOG.md changes
run: git commit --allow-empty -m "Update changelog placeholder" CHANGELOG.md
- name: Update package.json versions
uses: ./pkg/build/actions/bump-version
with:
version: ${{ inputs.version }}
- name: add package.json changes
- name: Add package.json changes
run: |
git add .
git commit -m "Update version to ${{ inputs.version }}"
- name: git push
- name: Git push
if: ${{ inputs.dry_run }} != true
run: git push
- name: Create PR without backports
if: "${{ inputs.backport == '' }}"
run: >
gh pr create \
$( (( ${{ inputs.latest }} == "true" )) && printf %s '-l "release/latest"') \
$( [ "x${{ inputs.latest }}" == "xtrue" ] && printf %s '-l "release/latest"') \
--dry-run=${{ inputs.dry_run }} \
-B "${{ inputs.target }}" \
--title "Release: ${{ inputs.version }}" \
--body "These code changes must be merged after a release is complete"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create PR with backports
if: "${{ inputs.backport != '' }}"
run: >
gh pr create \
$( (( ${{ inputs.latest }} == "true" )) && printf %s '-l "release/latest"') \
$( [ "x${{ inputs.latest }}" == "xtrue" ] && printf %s '-l "release/latest"') \
-l "backport ${{ inputs.backport }}" \
-l "product-approved" \
--dry-run=${{ inputs.dry_run }} \