From f8b092aba62115fe28116ddde663ff9909e9bf31 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Fri, 19 Jul 2024 18:27:59 +0200 Subject: [PATCH] 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 --- .../workflows/actions/changelog/action.yml | 10 +-- .github/workflows/actions/changelog/index.js | 56 ++++++------ .github/workflows/changelog.yml | 85 ++++++++++++++++--- .github/workflows/release-pr.yml | 71 ++++++++++++++-- 4 files changed, 171 insertions(+), 51 deletions(-) diff --git a/.github/workflows/actions/changelog/action.yml b/.github/workflows/actions/changelog/action.yml index 6e18d0dd623..c99bed23450 100644 --- a/.github/workflows/actions/changelog/action.yml +++ b/.github/workflows/actions/changelog/action.yml @@ -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: diff --git a/.github/workflows/actions/changelog/index.js b/.github/workflows/actions/changelog/index.js index 8d1d6be4360..9bb200ba208 100644 --- a/.github/workflows/actions/changelog/index.js +++ b/.github/workflows/actions/changelog/index.js @@ -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); } - diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 40f4bca5b66..e85173eec19 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -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 "" + cat CHANGELOG.part + echo "" + 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 diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 17a131c0f34..7fc1436a5e4 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -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 "" + cat CHANGELOG.part + echo "" + 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 }} \