Skip to content

Promote main to release #59

Promote main to release

Promote main to release #59

name: Promote main to release
# `main` is the integration branch (trunk); `release` is the release-ready branch.
# Open one daily promotion PR and let the main ruleset's required `CI Passed`
# check decide when it is safe to merge. A red run leaves the PR open for a
# human to diagnose; a later fix on `main` re-runs the promotion CI.
on:
schedule:
# After the 06:17 UTC nightly full run, and off the top of the hour to avoid
# GitHub Actions' busiest cron window.
- cron: '17 8 * * *'
workflow_dispatch:
concurrency:
group: promote-main-to-release
cancel-in-progress: false
permissions:
contents: read
pull-requests: write
jobs:
promote:
runs-on: ubuntu-latest
steps:
- name: Require promotion App credentials
env:
APP_ID: ${{ secrets.RELEASE_APP_ID }}
APP_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
run: |
if [ -z "$APP_ID" ] || [ -z "$APP_KEY" ]; then
echo "RELEASE_APP_ID / RELEASE_APP_PRIVATE_KEY are required to open promotion PRs and trigger CI" >&2
exit 1
fi
# copse-release-bot installation token in place of the SYNC_PR_TOKEN PAT.
# Contents: write as well as Pull requests: write — opening the PR needs
# only the latter, but enabling auto-merge is a merge and wants both.
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Open promotion PR and enable auto-merge
uses: actions/github-script@v9
with:
# GITHUB_TOKEN-created PRs do not trigger downstream workflows, and
# this organization blocks that token from creating PRs. The App is
# a distinct actor, so neither restriction applies to it.
github-token: ${{ steps.app-token.outputs.token }}
script: |
const { owner, repo } = context.repo;
const base = 'release';
const head = 'main';
// A promotion is complete only when every `main` commit is an
// ancestor of `release`. Comparing trees would mistake a squash merge
// for a complete promotion even though it discards that ancestry.
const comparison = await github.request(
'GET /repos/{owner}/{repo}/compare/{basehead}',
{ owner, repo, basehead: `${base}...${head}` },
);
if (comparison.data.ahead_by === 0) {
core.info(`${head} has no commits to promote.`);
return;
}
const open = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
base,
head: `${owner}:${head}`,
per_page: 100,
});
if (open.length > 1) {
core.setFailed(`Found ${open.length} open ${head} -> ${base} pull requests.`);
return;
}
let pull = open[0];
if (!pull) {
const created = await github.rest.pulls.create({
owner,
repo,
base,
head,
title: 'Promote main to release',
body: [
'Daily automated promotion of `main` to the release-ready `release` branch.',
'',
'The repository ruleset requires the full `CI Passed` gate. This PR will',
'merge automatically when that gate is green; failures leave it open',
'until the underlying issue is fixed on `main`.',
].join('\n'),
});
pull = created.data;
core.info(`Opened promotion PR #${pull.number}.`);
} else {
core.info(`Reusing promotion PR #${pull.number}.`);
}
if (pull.draft) {
core.setFailed(`Promotion PR #${pull.number} is unexpectedly a draft.`);
return;
}
if (pull.auto_merge) {
core.info(`Auto-merge is already enabled on PR #${pull.number}.`);
return;
}
await github.graphql(
`mutation EnablePromotionAutoMerge($pullRequestId: ID!) {
enablePullRequestAutoMerge(input: {
pullRequestId: $pullRequestId
mergeMethod: MERGE
}) {
pullRequest { number }
}
}`,
{ pullRequestId: pull.node_id },
);
core.info(`Enabled merge auto-merge on PR #${pull.number}.`);