mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
4728f2da91
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com> Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com>
178 lines
8.5 KiB
YAML
178 lines
8.5 KiB
YAML
name: Action Version Bump
|
|
|
|
# Guards the GitHub Action's independent release cadence (AGENTS.md → "GitHub
|
|
# Action versioning"). When a change touches the action's release surface
|
|
# (action.yml + the scripts it shells out to), this either recommends a version
|
|
# bump on the PR or — opt-in — performs it on merge to main.
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [main]
|
|
types: [opened, synchronize, reopened, edited]
|
|
push:
|
|
branches: [main]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
recommend:
|
|
name: Recommend bump on PR
|
|
if: ${{ github.event_name == 'pull_request' }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
# A newer push to the same PR obsoletes the in-flight recommendation.
|
|
concurrency:
|
|
group: action-version-bump-recommend-${{ github.event.pull_request.number }}
|
|
cancel-in-progress: true
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
|
|
- name: Detect action changes and compute recommended bump
|
|
id: detect
|
|
env:
|
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
ACTION_COMMIT_SUBJECT: ${{ github.event.pull_request.title }}
|
|
run: |
|
|
# Assign separately from `export` so errexit sees a failing git diff
|
|
# instead of silently treating it as "no files changed".
|
|
changed_files="$(git diff --name-only "${BASE_SHA}...${HEAD_SHA}")"
|
|
all_tags="$(git tag --list 'v*')"
|
|
export ACTION_CHANGED_FILES="${changed_files}"
|
|
export ACTION_ALL_TAGS="${all_tags}"
|
|
node scripts/recommend-action-version-bump.mjs "${RUNNER_TEMP}/action-version-comment.md"
|
|
|
|
- name: Upsert recommendation comment
|
|
uses: actions/github-script@v7
|
|
env:
|
|
ACTION_CHANGED: ${{ steps.detect.outputs.changed }}
|
|
COMMENT_FILE: ${{ runner.temp }}/action-version-comment.md
|
|
with:
|
|
script: |
|
|
const fs = require("node:fs");
|
|
const marker = "<!-- react-doctor:action-version -->";
|
|
const { owner, repo } = context.repo;
|
|
const issueNumber = context.payload.pull_request.number;
|
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
owner,
|
|
repo,
|
|
issue_number: issueNumber,
|
|
});
|
|
const existing = comments.find((comment) => comment.body?.includes(marker));
|
|
const actionChanged = process.env.ACTION_CHANGED === "true";
|
|
// Read outside the try: a missing comment file when the detect step
|
|
// reported changed=true is a contract violation, not a token issue,
|
|
// and should red the check.
|
|
const body = actionChanged ? fs.readFileSync(process.env.COMMENT_FILE, "utf8") : "";
|
|
// Comment writes are best-effort: on a fork PR the GITHUB_TOKEN is
|
|
// read-only regardless of the declared permissions, and a 403 here
|
|
// must not red the check for an external contributor.
|
|
try {
|
|
if (!actionChanged) {
|
|
// The PR no longer touches the action surface — clear a stale recommendation.
|
|
if (existing) {
|
|
await github.rest.issues.deleteComment({ owner, repo, comment_id: existing.id });
|
|
}
|
|
return;
|
|
}
|
|
if (existing) {
|
|
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
|
} else {
|
|
await github.rest.issues.createComment({ owner, repo, issue_number: issueNumber, body });
|
|
}
|
|
} catch (error) {
|
|
core.warning(`Could not upsert the action-version comment: ${error.message}`);
|
|
}
|
|
|
|
bump:
|
|
name: Perform bump on merge
|
|
# Opt-in: set the repo variable AUTO_BUMP_ACTION_TAG=true to auto-tag.
|
|
# Otherwise this is a no-op and the maintainer cuts the GPG-signed tag by
|
|
# hand using the commands from the PR recommendation.
|
|
if: ${{ github.event_name == 'push' && vars.AUTO_BUMP_ACTION_TAG == 'true' }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
# Serialize bump runs, with `queue: max` so a superseded PENDING run stays
|
|
# queued instead of canceled — under the default single pending slot, the
|
|
# middle of three quick merges would never be judged for a tag (an untagged
|
|
# action release). Queued runs are NOT guaranteed to execute in order, so
|
|
# the tag step below refuses to move the floating major backwards.
|
|
# `queue: max` requires cancel-in-progress to be false/absent.
|
|
concurrency:
|
|
group: action-version-bump-main
|
|
queue: max
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
|
|
- name: Detect action changes and compute recommended bump
|
|
id: detect
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
BEFORE_SHA: ${{ github.event.before }}
|
|
HEAD_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
|
run: |
|
|
base="${BEFORE_SHA}"
|
|
if [ -z "${base}" ] || [ "${base}" = "0000000000000000000000000000000000000000" ]; then
|
|
# New branch / first push: diff against HEAD's parent, or the empty
|
|
# tree for a root commit with no parent (so its files still count as
|
|
# changed instead of diffing HEAD against itself).
|
|
base="$(git rev-parse HEAD^ 2>/dev/null || git hash-object -t tree /dev/null)"
|
|
fi
|
|
# Classify from the merged PR's title — the same authoritative signal the
|
|
# recommend job uses — so a non-conventional merge subject (e.g.
|
|
# "Merge pull request #N") can't silently downgrade the bump to a patch.
|
|
# Fall back to the commit subject for a direct push to main.
|
|
pr_title="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" --jq '.[0].title // ""' 2>/dev/null || true)"
|
|
if [ -n "${pr_title}" ]; then
|
|
export ACTION_COMMIT_SUBJECT="${pr_title}"
|
|
else
|
|
export ACTION_COMMIT_SUBJECT="$(printf '%s' "${HEAD_COMMIT_MESSAGE}" | head -n1)"
|
|
fi
|
|
# Assign separately from `export` so errexit sees a failing git diff
|
|
# (e.g. an unreachable `github.event.before` after a force-push)
|
|
# instead of silently treating it as "no files changed".
|
|
changed_files="$(git diff --name-only "${base}" "${GITHUB_SHA}")"
|
|
all_tags="$(git tag --list 'v*')"
|
|
export ACTION_CHANGED_FILES="${changed_files}"
|
|
export ACTION_ALL_TAGS="${all_tags}"
|
|
node scripts/recommend-action-version-bump.mjs
|
|
|
|
- name: Create and push action release tags
|
|
if: ${{ steps.detect.outputs.changed == 'true' && steps.detect.outputs['tag-exists'] != 'true' }}
|
|
env:
|
|
NEXT_TAG: ${{ steps.detect.outputs.next }}
|
|
MAJOR_TAG: ${{ steps.detect.outputs.major }}
|
|
BUMP_LEVEL: ${{ steps.detect.outputs.level }}
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
# CI has no signing key, so override the repo's tag.gpgsign default and
|
|
# create an unsigned annotated tag instead of failing on a missing key.
|
|
git -c tag.gpgsign=false tag -a "${NEXT_TAG}" -m "react-doctor action ${NEXT_TAG} (${BUMP_LEVEL})"
|
|
git push origin "${NEXT_TAG}"
|
|
# `queue: max` serializes runs but does not order them: an older
|
|
# commit's run can execute after a newer commit already tagged and
|
|
# moved the floating major forward. Never move it backwards.
|
|
if git rev-parse --quiet --verify "refs/tags/${MAJOR_TAG}" >/dev/null \
|
|
&& ! git merge-base --is-ancestor "${MAJOR_TAG}^{commit}" "${GITHUB_SHA}"; then
|
|
echo "::warning::${MAJOR_TAG} points at a commit that is not an ancestor of ${GITHUB_SHA} (out-of-order bump runs); tagged ${NEXT_TAG} without moving ${MAJOR_TAG}."
|
|
echo "Tagged ${NEXT_TAG}; left ${MAJOR_TAG} in place (a newer commit already carries it)." >> "${GITHUB_STEP_SUMMARY}"
|
|
exit 0
|
|
fi
|
|
git -c tag.gpgsign=false tag -fa "${MAJOR_TAG}" -m "react-doctor action ${MAJOR_TAG} (floating major -> ${NEXT_TAG})"
|
|
git push --force origin "${MAJOR_TAG}"
|
|
echo "Tagged ${NEXT_TAG} and moved ${MAJOR_TAG} to ${GITHUB_SHA}." >> "${GITHUB_STEP_SUMMARY}"
|