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
11 changes: 11 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# actionlint configuration for the kit's own CI.
#
# The reusable workflows check out the kit itself with the job.workflow_repository
# and job.workflow_sha context properties (the called workflow's repository and
# commit, independent of the caller). GitHub documents them at
# https://docs.github.com/en/actions/reference/workflows-and-actions/contexts
# but actionlint (<= 1.7.12) does not know them yet. Ignore only that message.
paths:
.github/workflows/**/*.yaml:
ignore:
- 'property "workflow_(repository|sha)" is not defined in object type'
141 changes: 141 additions & 0 deletions .github/workflows/backport.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
name: backport

# shell: bash gives -eo pipefail, so a script failing before a pipe (classify |
# tee) fails the step; the implicit default is bash -e without pipefail.
defaults:
run:
shell: bash

# Reusable workflow: cherry-pick a merged main PR into the release branch named
# by its `backport release-X.Y.Z` label(s) and open the backport PR.
#
# The caller's stub (templates/workflows/backport.yaml) runs on
# pull_request_target closed/labeled for main. One label = one target = one PR;
# several labels make several PRs. Targets that already have a PR are skipped,
# so re-labelling never duplicates. The label of a released branch no longer
# exists (GA deletes it), so a released branch cannot be targeted.
#
# On a conflict the action commits the conflicted state as a *draft* PR and
# comments the resolution steps. The last step labels such drafts
# `backport-manual` and pings Slack; the developer resets that commit and redoes
# `git cherry-pick -x` by hand.
#
# Uses the release App token, not GITHUB_TOKEN: PRs created with GITHUB_TOKEN
# do not trigger other workflows, which would leave the backport PR without
# its required checks and therefore unmergeable.
#
# Variables (all prefixed CLOUD_COMPONENT_): RELEASE_APP_ID, RELEASE_TEAM, SLACK_CHANNEL_ID.
# Secrets (inherit): CLOUD_COMPONENT_RELEASE_APP_PRIVATE_KEY, SLACK_OAUTH_TOKEN.

on:
workflow_call:
secrets:
CLOUD_COMPONENT_RELEASE_APP_PRIVATE_KEY:
required: true
SLACK_OAUTH_TOKEN:
required: false

jobs:
backport:
name: backport PR #${{ github.event.pull_request.number }}
if: >
github.event.pull_request.merged == true &&
contains(toJSON(github.event.pull_request.labels.*.name), '"backport release-')
runs-on: ubuntu-latest
steps:
- name: Require release variables
env:
RELEASE_APP_ID: ${{ vars.CLOUD_COMPONENT_RELEASE_APP_ID }}
RELEASE_TEAM: ${{ vars.CLOUD_COMPONENT_RELEASE_TEAM }}
run: |
for v in RELEASE_APP_ID RELEASE_TEAM; do
[ -n "${!v}" ] || { echo "::error::variable CLOUD_COMPONENT_$v is empty; set it as an organization (or repository) variable"; exit 1; }
done

- name: Mint release App token
id: app
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.CLOUD_COMPONENT_RELEASE_APP_ID }}
private-key: ${{ secrets.CLOUD_COMPONENT_RELEASE_APP_PRIVATE_KEY }}

# The base branch (main), not the PR head: pull_request_target runs with
# secrets, so untrusted PR code must never be checked out here.
- uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ steps.app.outputs.token }}

- name: Check out the release kit
uses: actions/checkout@v7
with:
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
path: .release-kit

- name: Cherry-pick and open backport PRs
id: bp
uses: korthout/backport-action@v4
with:
github_token: ${{ steps.app.outputs.token }}
label_pattern: '^backport (release-[0-9]+\.[0-9]+\.[0-9]+)$'
pull_title: '${pull_title}'
# The trailer is repeated in the PR body so it survives the squash
# merge whichever commit-message default the repository uses.
# release-notes.sh needs it on the release-branch commit to tell a
# backport from a new change.
pull_description: |-
Backport of #${pull_number} to `${target_branch}`.

Created by the backport workflow from the merged main commit; the
cherry-pick trailer is the provenance the release-policy check verifies.

(cherry picked from commit ${{ github.event.pull_request.merge_commit_sha }})
add_author_as_assignee: true
add_team_reviewers: ${{ vars.CLOUD_COMPONENT_RELEASE_TEAM }}
copy_labels_pattern: '^(release-blocker|tag-rc)$'
auto_merge_enabled: true
auto_merge_method: squash
merge_commits: fail
git_committer_name: ${{ steps.app.outputs.app-slug }}[bot]
git_committer_email: ${{ steps.app.outputs.app-slug }}[bot]@users.noreply.github.com
experimental: '{"conflict_resolution":"draft_commit_conflicts"}'

# Drafts are conflicts in this mode: label them so release-policy accepts a
# hand-resolved (non-identical) cherry-pick, and tell the channel.
- name: Label conflicting drafts and notify
if: always()
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
SLACK_OAUTH_TOKEN: ${{ secrets.SLACK_OAUTH_TOKEN }}
SLACK_CHANNEL_ID: ${{ vars.CLOUD_COMPONENT_SLACK_CHANNEL_ID }}
CREATED: ${{ steps.bp.outputs.created_pull_numbers }}
SRC: ${{ github.event.pull_request.number }}
run: |
repo=${GITHUB_REPOSITORY#*/}
src_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$SRC"
src_title=$(gh pr view "$SRC" --json title -q .title 2>/dev/null || echo "")
if [ "${{ steps.bp.outcome }}" != "success" ]; then
.release-kit/scripts/slack-notify.sh --color danger --link "$src_url|source PR" \
--title ":x: *$repo* · backport of #$SRC failed" \
"• $src_title
• The backport bot left a comment on the source PR with the reason"
fi
for n in $(tr ',' ' ' <<<"$CREATED"); do
[ -n "$n" ] || continue
base=$(gh pr view "$n" --json baseRefName -q .baseRefName)
pr_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$n"
if [ "$(gh pr view "$n" --json isDraft -q .isDraft)" = "true" ]; then
gh pr edit "$n" --add-label backport-manual
.release-kit/scripts/slack-notify.sh --color warning --link "$pr_url|draft PR #$n" --link "$src_url|source PR" \
--title ":warning: *$repo* · backport #$SRC → \`$base\` has conflicts" \
"• $src_title
• Draft PR #$n holds the conflicted state; redo the cherry-pick by hand (\`git cherry-pick -x\`), push, mark ready
• Labelled \`backport-manual\` so release-policy accepts the hand-resolved content"
else
.release-kit/scripts/slack-notify.sh --color info --link "$pr_url|PR #$n" --link "$src_url|source PR" \
--title ":arrow_right: *$repo* · backport #$SRC → \`$base\`" \
"• $src_title
• Auto-merge is on: one release-manager approval merges it once the required checks are green"
fi
done
49 changes: 49 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: ci

# The kit's own checks: shell scripts (shellcheck + bats), workflows and
# templates (actionlint). Runs on every PR and on main.

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

env:
ACTIONLINT_VERSION: 1.7.12
BATS_VERSION: v1.14.0

jobs:
shell:
name: shellcheck + bats
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install shellcheck and bats
run: |
sudo apt-get update -q
sudo apt-get install -y -q shellcheck
git clone -q --depth 1 -b "$BATS_VERSION" https://github.com/bats-core/bats-core.git /tmp/bats
sudo /tmp/bats/install.sh /usr/local
- name: shellcheck
run: shellcheck -x -P SCRIPTDIR scripts/*.sh tests/helpers.bash
- name: bats
run: |
git config --global user.name ci
git config --global user.email ci@example.com
bats tests

workflows:
name: actionlint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install actionlint
run: |
bash <(curl -sSfL "https://raw.githubusercontent.com/rhysd/actionlint/v${ACTIONLINT_VERSION}/scripts/download-actionlint.bash") "$ACTIONLINT_VERSION" /usr/local/bin
- name: actionlint (kit workflows)
run: actionlint -color .github/workflows/*.yaml
- name: actionlint (caller templates)
run: actionlint -color templates/workflows/*.yaml
143 changes: 143 additions & 0 deletions .github/workflows/cut-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: cut-release

# shell: bash gives -eo pipefail, so a script failing before a pipe (classify |
# tee) fails the step; the implicit default is bash -e without pipefail.
defaults:
run:
shell: bash

# Reusable workflow: cut a release branch and tag vX.Y.Z-rc1.
#
# Called from a repository's .github/workflows/cut-release.yaml stub
# (templates/workflows/cut-release.yaml) on workflow_dispatch. The kind decides
# the number and the starting point (scripts/cut-release.sh):
# minor release-X.(Y+1).0 from main everything merged since the last GA
# patch release-X.Y.(Z+1) from the GA tag fixes only, backported afterwards
# major release-(X+1).0.0 from main an explicit decision
# The tag push starts the rc path of release.yaml; this workflow builds nothing.
#
# Branch and tag are pushed with the release GitHub App's token, the only actor
# (besides the release-manager team) the rulesets allow to create release-*
# branches and v* tags. Because the App bypasses the rulesets, the workflow
# itself checks that a release manager pressed the button (App permission
# "Members: read").
#
# Variables (all prefixed CLOUD_COMPONENT_): RELEASE_APP_ID, RELEASE_TEAM, SLACK_CHANNEL_ID.
# Secrets (inherit): CLOUD_COMPONENT_RELEASE_APP_PRIVATE_KEY, SLACK_OAUTH_TOKEN.

on:
workflow_call:
inputs:
kind:
description: "minor | patch | major"
required: true
type: string
version:
description: "Override the computed X.Y.Z (first release, or an explicit number)"
required: false
default: ""
type: string
secrets:
CLOUD_COMPONENT_RELEASE_APP_PRIVATE_KEY:
required: true
SLACK_OAUTH_TOKEN:
required: false
outputs:
version:
description: "X.Y.Z that was cut"
value: ${{ jobs.cut.outputs.version }}
branch:
description: "release-X.Y.Z"
value: ${{ jobs.cut.outputs.branch }}
tag:
description: "vX.Y.Z-rc1"
value: ${{ jobs.cut.outputs.tag }}

jobs:
cut:
name: cut ${{ inputs.kind }} ${{ inputs.version }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.cut.outputs.version }}
branch: ${{ steps.cut.outputs.branch }}
tag: ${{ steps.cut.outputs.tag }}
env:
RELEASE_TEAM: ${{ vars.CLOUD_COMPONENT_RELEASE_TEAM }}
steps:
- name: Require release variables
env:
RELEASE_APP_ID: ${{ vars.CLOUD_COMPONENT_RELEASE_APP_ID }}
run: |
for v in RELEASE_APP_ID RELEASE_TEAM; do
[ -n "${!v}" ] || { echo "::error::variable CLOUD_COMPONENT_$v is empty; set it as an organization (or repository) variable"; exit 1; }
done

- name: Mint release App token
id: app
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.CLOUD_COMPONENT_RELEASE_APP_ID }}
private-key: ${{ secrets.CLOUD_COMPONENT_RELEASE_APP_PRIVATE_KEY }}

- name: Require release-manager team membership
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
run: |
if ! gh api "orgs/${{ github.repository_owner }}/teams/${RELEASE_TEAM}/memberships/${{ github.actor }}" \
--jq '.state' 2>/dev/null | grep -qx active; then
echo "::error::${{ github.actor }} is not an active member of ${{ github.repository_owner }}/${RELEASE_TEAM}"
exit 1
fi

- uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ steps.app.outputs.token }}

- name: Check out the release kit
uses: actions/checkout@v7
with:
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
path: .release-kit

- name: Cut branch, tag rc1, create backport label
id: cut
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
BOT: ${{ steps.app.outputs.app-slug }}[bot]
KIND: ${{ inputs.kind }}
VERSION_OVERRIDE: ${{ inputs.version }}
run: |
git config user.name "$BOT"
git config user.email "$BOT@users.noreply.github.com"
args=("$KIND")
[ -n "$VERSION_OVERRIDE" ] && args+=(--version "$VERSION_OVERRIDE")
.release-kit/scripts/cut-release.sh "${args[@]}"

- name: Notify Slack
if: always()
env:
SLACK_OAUTH_TOKEN: ${{ secrets.SLACK_OAUTH_TOKEN }}
SLACK_CHANNEL_ID: ${{ vars.CLOUD_COMPONENT_SLACK_CHANNEL_ID }}
BRANCH: ${{ steps.cut.outputs.branch }}
TAG: ${{ steps.cut.outputs.tag }}
KIND: ${{ inputs.kind }}
VERSION_OVERRIDE: ${{ inputs.version }}
run: |
if [ "${{ job.status }}" = "success" ]; then
case $KIND in
patch) from="the newest GA tag (fixes only)";;
*) from="\`main\` (everything merged since the last GA)";;
esac
.release-kit/scripts/slack-notify.sh --color info \
--link "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/$BRANCH|branch" \
--title ":scissors: *${GITHUB_REPOSITORY#*/}* · *$BRANCH* · $KIND cut" \
"• Branch \`$BRANCH\` created from $from
• \`$TAG\` is publishing to Harbor
• To get a fix into this release, label its \`main\` PR \`backport $BRANCH\`"
else
.release-kit/scripts/slack-notify.sh --color danger \
--title ":x: *${GITHUB_REPOSITORY#*/}* · cut-release $KIND $VERSION_OVERRIDE failed" \
"See the workflow run for the reason. Nothing was created."
fi
Loading