Skip to content
Open
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
80 changes: 80 additions & 0 deletions .github/actions/prepare-mergeback-branch/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: "Prepare mergeback branch"
description: Prepares a mergeback branch and opens a PR for it
inputs:
base:
description: "The name of the base branch"
required: true
head:
description: "The name of the head branch"
required: true
branch:
description: "The name of the branch to create."
required: true
version:
description: "The new version"
required: true
token:
description: "The token to use"
required: true
dry-run:
description: "Set to true to skip creating the PR. The branch will still be pushed."
default: "false"
runs:
using: composite
steps:
- name: Create mergeback branch
shell: bash
env:
VERSION: "${{ inputs.version }}"
NEW_BRANCH: "${{ inputs.branch }}"
run: |
set -exu

# Ensure we are on the new branch
git checkout "${NEW_BRANCH}"

# Update the version number ready for the next release
npm version patch --no-git-tag-version

# Update the changelog, adding a new version heading directly above the most recent existing one
awk '!f && /##/{print "'"## [UNRELEASED]\n\nNo user facing changes.\n"'"; f=1}1' CHANGELOG.md > temp && mv temp CHANGELOG.md
git add .
git commit -m "Update changelog and version after ${VERSION}"

git push origin "${NEW_BRANCH}"

- name: Create PR
shell: bash
if: inputs.dry-run != 'true'
env:
VERSION: "${{ inputs.version }}"
BASE_BRANCH: "${{ inputs.base }}"
HEAD_BRANCH: "${{ inputs.head }}"
NEW_BRANCH: "${{ inputs.branch }}"
GITHUB_TOKEN: "${{ inputs.token }}"
run: |
set -exu
pr_title="Mergeback ${VERSION} ${HEAD_BRANCH} into ${BASE_BRANCH}"
pr_body=$(cat << EOF
This PR bumps the version number and updates the changelog after the ${VERSION} release.

Please do the following:

- [ ] Remove and re-add the "Rebuild" label to the PR to trigger just this workflow.
- [ ] Wait for the "Rebuild" workflow to push a commit updating the distribution files.
- [ ] Mark the PR as ready for review to trigger the full set of PR checks.
- [ ] Approve and merge the PR. When merging the PR, make sure "Create a merge commit" is
selected rather than "Squash and merge" or "Rebase and merge".
EOF
)

# PR checks won't be triggered on PRs created by Actions. Therefore mark the PR as draft
# so that a maintainer can take the PR out of draft, thereby triggering the PR checks.
gh pr create \
--head "${NEW_BRANCH}" \
--base "${BASE_BRANCH}" \
--title "${pr_title}" \
--label "Rebuild" \
--body "${pr_body}" \
--assignee "${GITHUB_ACTOR}" \
--draft
49 changes: 8 additions & 41 deletions .github/workflows/post-release-mergeback.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,48 +124,15 @@ jobs:
cat $PARTIAL_CHANGELOG
echo "::endgroup::"

- name: Create mergeback branch
- name: Create mergeback branch and PR
if: ${{ steps.check.outputs.exists != 'true' && endsWith(github.ref_name, steps.getVersion.outputs.latest_release_branch) }}
env:
VERSION: "${{ steps.getVersion.outputs.version }}"
NEW_BRANCH: "${{ steps.getVersion.outputs.newBranch }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
run: |
set -exu
pr_title="Mergeback ${VERSION} ${HEAD_BRANCH} into ${BASE_BRANCH}"
pr_body=$(cat << EOF
This PR bumps the version number and updates the changelog after the ${VERSION} release.

Please do the following:

- [ ] Remove and re-add the "Rebuild" label to the PR to trigger just this workflow.
- [ ] Wait for the "Rebuild" workflow to push a commit updating the distribution files.
- [ ] Mark the PR as ready for review to trigger the full set of PR checks.
- [ ] Approve and merge the PR. When merging the PR, make sure "Create a merge commit" is
selected rather than "Squash and merge" or "Rebase and merge".
EOF
)

# Update the version number ready for the next release
npm version patch --no-git-tag-version

# Update the changelog, adding a new version heading directly above the most recent existing one
awk '!f && /##/{print "'"## [UNRELEASED]\n\nNo user facing changes.\n"'"; f=1}1' CHANGELOG.md > temp && mv temp CHANGELOG.md
git add .
git commit -m "Update changelog and version after ${VERSION}"

git push origin "${NEW_BRANCH}"

# PR checks won't be triggered on PRs created by Actions. Therefore mark the PR as draft
# so that a maintainer can take the PR out of draft, thereby triggering the PR checks.
gh pr create \
--head "${NEW_BRANCH}" \
--base "${BASE_BRANCH}" \
--title "${pr_title}" \
--label "Rebuild" \
--body "${pr_body}" \
--assignee "${GITHUB_ACTOR}" \
--draft
uses: ./.github/actions/prepare-mergeback-branch
with:
base: "${{ env.BASE_BRANCH }}"
head: "${{ env.HEAD_BRANCH }}"
branch: "${{ steps.getVersion.outputs.newBranch }}"
version: "${{ steps.getVersion.outputs.version }}"
token: "${{ secrets.GITHUB_TOKEN }}"

- name: Generate token
uses: actions/create-github-app-token@v2.1.1
Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Prepare release
on:
workflow_call:
outputs:
version:
description: "The version that is being released."
value: ${{ jobs.prepare.outputs.version }}
major_version:
description: "The major version of the release."
value: ${{ jobs.prepare.outputs.major_version }}
latest_tag:
description: "The most recent, existing release tag."
value: ${{ jobs.prepare.outputs.latest_tag }}
backport_source_branch:
description: "The release branch for the given tag."
value: ${{ jobs.prepare.outputs.backport_source_branch }}
backport_target_branches:
description: "JSON encoded list of branches to target with backports."
value: ${{ jobs.prepare.outputs.backport_target_branches }}

push:
paths:
- .github/workflows/prepare-release.yml

jobs:
prepare:
name: "Prepare release"
runs-on: ubuntu-latest
if: github.repository == 'github/codeql-action'

permissions:
contents: read

outputs:
version: ${{ steps.versions.outputs.version }}
major_version: ${{ steps.versions.outputs.major_version }}
latest_tag: ${{ steps.versions.outputs.latest_tag }}
backport_source_branch: ${{ steps.branches.outputs.backport_source_branch }}
backport_target_branches: ${{ steps.branches.outputs.backport_target_branches }}

steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0 # Need full history for calculation of diffs

- name: Configure runner for release
uses: ./.github/actions/release-initialise

- name: Get version tags
id: versions
run: |
VERSION="v$(jq '.version' -r 'package.json')"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
MAJOR_VERSION=$(cut -d '.' -f1 <<< "${VERSION}")
echo "major_version=${MAJOR_VERSION}" >> $GITHUB_OUTPUT
LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+' | head -1)
echo "latest_tag=${LATEST_TAG}" >> $GITHUB_OUTPUT

- name: Determine older release branches
id: branches
uses: ./.github/actions/release-branches
with:
major_version: ${{ steps.versions.outputs.major_version }}
latest_tag: ${{ steps.versions.outputs.latest_tag }}

- name: Print release information
run: |
echo 'version: ${{ steps.versions.outputs.version }}'
echo 'major_version: ${{ steps.versions.outputs.major_version }}'
echo 'latest_tag: ${{ steps.versions.outputs.latest_tag }}'
echo 'backport_source_branch: ${{ steps.branches.outputs.backport_source_branch }}'
echo 'backport_target_branches: ${{ steps.branches.outputs.backport_target_branches }}'
181 changes: 181 additions & 0 deletions .github/workflows/rollback-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
name: Rollback release
on:
# You can trigger this workflow via workflow dispatch to start a rollback.
# This will create a draft release that mirrors the release for `rollback-tag`.
workflow_dispatch:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is intended to be triggered on main?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's the intention. I think it could probably work if triggered from a different branch, but I haven't verified that.

inputs:
rollback-tag:
type: string
description: "The tag of an old release to roll-back to."
required: true
# Only for dry-runs of changes to the workflow.
push:
paths:
- .github/workflows/rollback-release.yml
- .github/actions/prepare-mergeback-branch/**

jobs:
prepare:
name: "Prepare release"
if: github.repository == 'github/codeql-action'

permissions:
contents: read

uses: ./.github/workflows/prepare-release.yml

rollback:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll soon be needing to release CodeQL Action v4 to run on Node 24. I think the backport automation in update-release-branch.yml would create necessary backport releases, but I think we might want a way to more quickly rollback a change across all active release branches (without waiting for the full set of PR checks). Perhaps it makes sense to matrix this job across all active release branches?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd propose doing this in a follow-up PR once we have the basic version of this workflow working.

name: "Create rollback release"
if: github.repository == 'github/codeql-action'
runs-on: ubuntu-latest
timeout-minutes: 45

# Don't set the deployment environment for test runs
# The Actions token does not have permissions to push changes to workflow files.
# Since workflow files may change as part of a backport PR, we use the "Automation" environment for real runs to authenticate as a GitHub App and push these changes.
environment: ${{ github.event_name == 'workflow_dispatch' && 'Automation' || '' }}

needs:
- prepare

permissions:
contents: write # needed to push to the repo (tags and releases)

steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0 # Need full history for calculation of diffs

- name: Configure runner for release
uses: ./.github/actions/release-initialise

- name: Create tag for testing
if: github.event_name != 'workflow_dispatch'
shell: bash
run: git tag v0.0.0

# We start by preparing the mergeback branch, mainly so that we have the updated changelog
# readily available for the partial changelog that's needed for the release.
- name: Prepare mergeback branch
id: mergeback-branch
env:
BASE_BRANCH: ${{ (github.event_name == 'workflow_dispatch' && 'main') || github.ref_name }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
set -x

# Checkout the base branch, since we may be testing on a different branch
git checkout "$BASE_BRANCH"

# Generate a new branch name for the mergeback PR
short_sha="${GITHUB_SHA:0:8}"
NEW_BRANCH="mergeback/${VERSION}-to-${BASE_BRANCH}-${short_sha}"
echo "new-branch=${NEW_BRANCH}" >> $GITHUB_OUTPUT

# Create the mergeback branch
git checkout -b "${NEW_BRANCH}"

- name: Prepare rollback changelog
env:
NEW_CHANGELOG: "${{ runner.temp }}/new_changelog.md"
# We usually expect to checkout `inputs.rollback-tag` (required for `workflow_dispatch`),
# but use `v0.0.0` for testing.
ROLLBACK_TAG: ${{ inputs.rollback-tag || 'v0.0.0' }}
LATEST_TAG: ${{ needs.prepare.outputs.latest_tag }}
VERSION: "${{ needs.prepare.outputs.version }}"
run: |
python .github/workflows/script/rollback_changelog.py \
--target-version "${ROLLBACK_TAG:1}" \
--rollback-version "${LATEST_TAG:1}" \
--new-version "$VERSION" > $NEW_CHANGELOG

echo "::group::New CHANGELOG"
cat $NEW_CHANGELOG
echo "::endgroup::"

- name: Create tags
shell: bash
env:
# We usually expect to checkout `inputs.rollback-tag` (required for `workflow_dispatch`),
# but use `v0.0.0` for testing.
ROLLBACK_TAG: ${{ inputs.rollback-tag || 'v0.0.0' }}
RELEASE_TAG: ${{ needs.prepare.outputs.version }}
MAJOR_VERSION_TAG: ${{ needs.prepare.outputs.major_version }}
run: |
git checkout "refs/tags/${ROLLBACK_TAG}"
git tag --annotate "${RELEASE_TAG}" --message "${RELEASE_TAG}"
git tag --annotate "${MAJOR_VERSION_TAG}" --message "${MAJOR_VERSION_TAG}" --force

- name: Push tags
# skip when testing
if: github.event_name == 'workflow_dispatch'
shell: bash
env:
RELEASE_TAG: ${{ needs.prepare.outputs.version }}
MAJOR_VERSION_TAG: ${{ needs.prepare.outputs.major_version }}
run: |
git push origin --atomic --force refs/tags/"${RELEASE_TAG}" refs/tags/"${MAJOR_VERSION_TAG}"

- name: Prepare partial Changelog
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could be clearer what we mean here by "partial changelog". It's still not fully clear, but we could call it "release description" for instance?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the existing terminology for this from the release workflow. Happy to change this to something else here, but we should probably update it there then as well to be consistent.

env:
NEW_CHANGELOG: "${{ runner.temp }}/new_changelog.md"
PARTIAL_CHANGELOG: "${{ runner.temp }}/partial_changelog.md"
VERSION: "${{ needs.prepare.outputs.version }}"
run: |
python .github/workflows/script/prepare_changelog.py $NEW_CHANGELOG "$VERSION" > $PARTIAL_CHANGELOG

echo "::group::Partial CHANGELOG"
cat $PARTIAL_CHANGELOG
echo "::endgroup::"

- name: Generate token
if: github.event_name == 'workflow_dispatch'
uses: actions/create-github-app-token@v2.1.1
id: app-token
with:
app-id: ${{ vars.AUTOMATION_APP_ID }}
private-key: ${{ secrets.AUTOMATION_PRIVATE_KEY }}

- name: Create the rollback release
if: github.event_name == 'workflow_dispatch'
env:
PARTIAL_CHANGELOG: "${{ runner.temp }}/partial_changelog.md"
VERSION: "${{ needs.prepare.outputs.version }}"
GH_TOKEN: ${{ steps.app-token.outputs.token }}
RELEASE_URL: "${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ needs.prepare.outputs.version }}"
run: |
set -exu

# Do not mark this release as latest. The most recent bundle release must be marked as latest.
# Set as a draft to give us an opportunity to review the rollback release.
gh release create \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this print a link to the new release? It might be nice to add a link to the job summary to make it easier to move forwards to the next step of reviewing and publishing the release.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it automatically does that, but I have pushed a change which computes the release URL and adds it to the job summary.

"$VERSION" \
--latest=false \
--draft \
--title "$VERSION" \
--notes-file "$PARTIAL_CHANGELOG"

echo "Created draft rollback release at $RELEASE_URL" >> $GITHUB_STEP_SUMMARY

- name: Update changelog
shell: bash
env:
NEW_CHANGELOG: "${{ runner.temp }}/new_changelog.md"
NEW_BRANCH: "${{ steps.mergeback-branch.outputs.new-branch }}"
run: |
git checkout "${NEW_BRANCH}"
mv ${NEW_CHANGELOG} CHANGELOG.md

- name: Create mergeback branch and PR
uses: ./.github/actions/prepare-mergeback-branch
with:
base: "main"
head: ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this blank?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prepare-mergeback-branch action only uses it as part of the mergeback PR title, where it is typically something like refs/heads/releases/v3. That didn't make sense here, because we are not touching the v3 branch.

branch: "${{ steps.mergeback-branch.outputs.new-branch }}"
version: "${{ needs.prepare.outputs.version }}"
token: "${{ secrets.GITHUB_TOKEN }}"
# Setting this to `true` for non-workflow_dispatch events will
# still push the `branch`, but won't create a corresponding PR
dry-run: "${{ github.event_name != 'workflow_dispatch' }}"

Loading
Loading