Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/git-commit-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:

- name: Set Up Node.js
id: set_up_nodejs
uses: actions/setup-node@v6.4.0
uses: actions/setup-node@v7.0.0
with:
node-version: "lts/*"

Expand Down
114 changes: 103 additions & 11 deletions .github/workflows/task-release-and-deploy-to-prod.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# GitHub Actions Workflow for 'Release and Deploy to Production' Task
#
# It creates a release VCS branch with the changes that are not yet in the production VCS reference,
# opens a draft GitHub pull request for it, and comments a release and deployment checklist on that
# pull request. When there is nothing to release, it ends quietly.
# It opens a GitHub pull request to create a release and deploy it to production. It ends quietly
# when there is nothing to release.

name: "Task: Release and Deploy to Production"

Expand All @@ -27,6 +26,13 @@ on:
kind: release
kind: deploy
description: Labels of the GitHub Pull Request, One per Line
request_reviews_from_change_authors:
type: boolean
required: false
default: true
description: >-
Whether to request a review of the GitHub pull request from every GitHub user who
authored one of the commits included in it.

pre_deployment_checklist_items:
type: string
Expand Down Expand Up @@ -85,13 +91,23 @@ jobs:
run: |
echo "Counting commits of '$RELEASE_VCS_REF' that are not in '$PRODUCTION_VCS_REF'…"
change_count="$(git rev-list --count "${PRODUCTION_VCS_REF}..${RELEASE_VCS_REF}" --)"
echo "change_count=${change_count:?}" >> "$GITHUB_OUTPUT"

if [ "${change_count:?}" -gt 0 ]; then
echo "There are $change_count changes to release."
echo 'has_changes=true' >> "$GITHUB_OUTPUT"
else
echo 'There is nothing to release. The remaining steps will be skipped.'
echo 'has_changes=false' >> "$GITHUB_OUTPUT"

{
echo '# Release and Deploy to Production'
echo
echo 'Nothing to release.'
echo
echo "- **From:** \`$RELEASE_VCS_REF\`"
echo "- **Into:** \`$PRODUCTION_VCS_REF\`"
} >> "$GITHUB_STEP_SUMMARY"
fi

- name: Prepare Release
Expand Down Expand Up @@ -120,6 +136,7 @@ jobs:
echo "changelog_file_path=${changelog_file_path:?}" >> "$GITHUB_OUTPUT"

- name: Create GitHub Pull Request
id: create_github_pull_request
if: ${{ steps.check_changes.outputs.has_changes == 'true' }}
run: |
label_args=()
Expand All @@ -129,16 +146,31 @@ jobs:
fi
done <<< "$GITHUB_PULL_REQUEST_LABELS"

gh pr create \
--base "$PRODUCTION_VCS_REF" \
--head "$RELEASE_VCS_REF" \
--draft \
--title 'Release and deploy to production' \
--body-file "$CHANGELOG_FILE_PATH" \
--assignee "$RELEASE_ASSIGNEE" \
"${label_args[@]}"
github_pull_request_url="$(
gh pr create \
--base "$PRODUCTION_VCS_REF" \
--head "$RELEASE_VCS_REF" \
--draft \
--title 'Release and deploy to production' \
--body-file "$CHANGELOG_FILE_PATH" \
--assignee "$RELEASE_ASSIGNEE" \
"${label_args[@]}"
)"

echo "url=${github_pull_request_url:?}" >> "$GITHUB_OUTPUT"

{
echo '# Release and Deploy to Production'
echo
echo "$github_pull_request_url"
echo
echo "- **Changes:** $CHANGE_COUNT"
echo "- **From:** \`$RELEASE_VCS_REF\`"
echo "- **Into:** \`$PRODUCTION_VCS_REF\`"
} >> "$GITHUB_STEP_SUMMARY"
env:
GH_TOKEN: ${{ github.token }}
CHANGE_COUNT: ${{ steps.check_changes.outputs.change_count }}
RELEASE_ASSIGNEE: ${{ github.actor }}
CHANGELOG_FILE_PATH: ${{ steps.generate_changelog.outputs.changelog_file_path }}
GITHUB_PULL_REQUEST_LABELS: ${{ inputs.github_pull_request_labels }}
Expand Down Expand Up @@ -174,3 +206,63 @@ jobs:
GH_TOKEN: ${{ github.token }}
PRE_DEPLOYMENT_CHECKLIST_ITEMS: ${{ inputs.pre_deployment_checklist_items }}
POST_DEPLOYMENT_CHECKLIST_ITEMS: ${{ inputs.post_deployment_checklist_items }}

- name: Request Reviews from Change Authors
if: >-
${{ steps.check_changes.outputs.has_changes == 'true'
&& inputs.request_reviews_from_change_authors }}
run: |
github_pull_request_number="$(
gh pr view "$GITHUB_PULL_REQUEST_URL" --json number --jq '.number'
)"

# The author of a commit is read from the GitHub API rather than from the Git history,
# because only GitHub can resolve the email address of a commit to a GitHub user. The
# commits are paginated, as a release easily exceeds the size of a single page.
echo "Getting the authors of the commits of pull request #$github_pull_request_number…"
commit_authors="$(
gh api --paginate \
"repos/${GITHUB_REPOSITORY:?}/pulls/${github_pull_request_number:?}/commits" \
--jq '.[].author.login // empty' \
| sort -u
)"

reviewers=()
while IFS= read -r commit_author; do
# A bot cannot be a reviewer, and neither can the pull request's own author, which is
# the bot of GitHub Actions because the pull request is created with its token.
case "$commit_author" in
'' | *'[bot]') continue ;;
esac
reviewers+=("$commit_author")
done <<< "$commit_authors"

# A review is requested from one user at a time, because GitHub rejects the whole request
# when a single one of the users cannot be a reviewer, for instance because they no longer
# have access to the VCS repository. A review that cannot be requested is not fatal.
requested_reviewers=()
rejected_reviewers=()
for reviewer in "${reviewers[@]}"; do
echo "Requesting a review from '$reviewer'…"
if gh pr edit "$GITHUB_PULL_REQUEST_URL" --add-reviewer "$reviewer"; then
requested_reviewers+=("@$reviewer")
else
echo "::warning::Could not request a review from '$reviewer'."
rejected_reviewers+=("@$reviewer")
fi
done

{
if [ "${#requested_reviewers[@]}" -gt 0 ]; then
echo "- **Reviewers:** ${requested_reviewers[*]}"
else
echo '- **Reviewers:** nobody'
fi

if [ "${#rejected_reviewers[@]}" -gt 0 ]; then
echo "- **Reviewers GitHub rejected:** ${rejected_reviewers[*]}"
fi
} >> "$GITHUB_STEP_SUMMARY"
env:
GH_TOKEN: ${{ github.token }}
GITHUB_PULL_REQUEST_URL: ${{ steps.create_github_pull_request.outputs.url }}
100 changes: 90 additions & 10 deletions .github/workflows/task-release-and-publish.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# GitHub Actions Workflow for 'Release and Publish' Task
#
# Once a release pull request has been merged into the staging VCS reference, it creates a
# publication VCS branch and opens a draft GitHub pull request that publishes the release.
# It opens a GitHub pull request to publish a previously created release. It must be called from a
# merged release pull request, whose title, head VCS reference and assignee it derives its own from.

name: "Task: Release and Publish"

Expand All @@ -20,6 +20,13 @@ on:
task
kind: publish
description: Labels of the GitHub Pull Request, One per Line
request_reviews_from_change_authors:
type: boolean
required: false
default: true
description: >-
Whether to request a review of the GitHub pull request from every GitHub user who
authored one of the commits included in it.

permissions: {}

Expand Down Expand Up @@ -72,6 +79,7 @@ jobs:
echo "CREATE_RELEASE_GITHUB_PULL_REQUEST_DESCRIPTION=${create_release_github_pull_request_description:?}" >> "$GITHUB_ENV"

- name: Create GitHub Pull Request for Publication
id: create_github_pull_request
run: |
label_args=()
while IFS= read -r label; do
Expand All @@ -80,14 +88,86 @@ jobs:
fi
done <<< "$GITHUB_PULL_REQUEST_LABELS"

gh pr create \
--base "$RELEASE_VCS_REF" \
--head "$CREATE_RELEASE_VCS_REF" \
--draft \
--title "$CREATE_RELEASE_GITHUB_PULL_REQUEST_TITLE" \
--body "$CREATE_RELEASE_GITHUB_PULL_REQUEST_DESCRIPTION" \
--assignee "$RELEASE_ASSIGNEE" \
"${label_args[@]}"
github_pull_request_url="$(
gh pr create \
--base "$RELEASE_VCS_REF" \
--head "$CREATE_RELEASE_VCS_REF" \
--draft \
--title "$CREATE_RELEASE_GITHUB_PULL_REQUEST_TITLE" \
--body "$CREATE_RELEASE_GITHUB_PULL_REQUEST_DESCRIPTION" \
--assignee "$RELEASE_ASSIGNEE" \
"${label_args[@]}"
)"

echo "url=${github_pull_request_url:?}" >> "$GITHUB_OUTPUT"

{
echo '# Release and Publish'
echo
echo "$github_pull_request_url"
echo
echo "- **From:** \`$CREATE_RELEASE_VCS_REF\`"
echo "- **Into:** \`$RELEASE_VCS_REF\`"
echo "- **Release:** $PREPARE_RELEASE_GITHUB_PULL_REQUEST_HTML_URL"
} >> "$GITHUB_STEP_SUMMARY"
env:
GH_TOKEN: ${{ github.token }}
GITHUB_PULL_REQUEST_LABELS: ${{ inputs.github_pull_request_labels }}

- name: Request Reviews from Change Authors
if: ${{ inputs.request_reviews_from_change_authors }}
run: |
github_pull_request_number="$(
gh pr view "$GITHUB_PULL_REQUEST_URL" --json number --jq '.number'
)"

# The author of a commit is read from the GitHub API rather than from the Git history,
# because only GitHub can resolve the email address of a commit to a GitHub user. The
# commits are paginated, as a release easily exceeds the size of a single page.
echo "Getting the authors of the commits of pull request #$github_pull_request_number…"
commit_authors="$(
gh api --paginate \
"repos/${GITHUB_REPOSITORY:?}/pulls/${github_pull_request_number:?}/commits" \
--jq '.[].author.login // empty' \
| sort -u
)"

reviewers=()
while IFS= read -r commit_author; do
# A bot cannot be a reviewer, and neither can the pull request's own author, which is
# the bot of GitHub Actions because the pull request is created with its token.
case "$commit_author" in
'' | *'[bot]') continue ;;
esac
reviewers+=("$commit_author")
done <<< "$commit_authors"

# A review is requested from one user at a time, because GitHub rejects the whole request
# when a single one of the users cannot be a reviewer, for instance because they no longer
# have access to the VCS repository. A review that cannot be requested is not fatal.
requested_reviewers=()
rejected_reviewers=()
for reviewer in "${reviewers[@]}"; do
echo "Requesting a review from '$reviewer'…"
if gh pr edit "$GITHUB_PULL_REQUEST_URL" --add-reviewer "$reviewer"; then
requested_reviewers+=("@$reviewer")
else
echo "::warning::Could not request a review from '$reviewer'."
rejected_reviewers+=("@$reviewer")
fi
done

{
if [ "${#requested_reviewers[@]}" -gt 0 ]; then
echo "- **Reviewers:** ${requested_reviewers[*]}"
else
echo '- **Reviewers:** nobody'
fi

if [ "${#rejected_reviewers[@]}" -gt 0 ]; then
echo "- **Reviewers GitHub rejected:** ${rejected_reviewers[*]}"
fi
} >> "$GITHUB_STEP_SUMMARY"
env:
GH_TOKEN: ${{ github.token }}
GITHUB_PULL_REQUEST_URL: ${{ steps.create_github_pull_request.outputs.url }}