Manual Release Bump #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Manual Release Bump | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: Target release tag or version, for example v0.1.1 or 0.1.1-beta.2 | |
| required: true | |
| type: string | |
| dry_run: | |
| description: Validate and prepare the bump without pushing a branch or opening a PR | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| actions: write | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| concurrency: | |
| group: manual-release-bump-master-${{ startsWith(inputs.tag, 'v') && inputs.tag || format('v{0}', inputs.tag) }} | |
| cancel-in-progress: false | |
| jobs: | |
| bump_release_version: | |
| if: github.repository == 'JeremyDev87/legolas' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: master | |
| fetch-depth: 0 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 | |
| with: | |
| node-version: "22" | |
| - name: Set up Rust toolchain | |
| run: | | |
| rustup toolchain install 1.95.0 --profile minimal | |
| rustup default 1.95.0 | |
| - name: Resolve bump metadata | |
| id: meta | |
| shell: bash | |
| env: | |
| INPUT_TAG: ${{ inputs.tag }} | |
| run: | | |
| tag="$INPUT_TAG" | |
| if [[ "$tag" != v* ]]; then | |
| tag="v$tag" | |
| fi | |
| current_version="$(node -p "require('./package.json').version")" | |
| current_cargo_version="$(awk -F '"' '/^version[[:space:]]*=[[:space:]]*"/ { print $2; exit }' crates/legolas-cli/Cargo.toml)" | |
| expected_version="${tag#v}" | |
| branch="$(node --input-type=module -e "import { createManualBumpBranchName } from './scripts/bump-release-version.mjs'; console.log(createManualBumpBranchName(process.argv[1]));" "$tag")" | |
| if [ -z "$current_cargo_version" ]; then | |
| echo "Unable to read crates/legolas-cli/Cargo.toml version" | |
| exit 1 | |
| fi | |
| if [ "$current_cargo_version" != "$current_version" ]; then | |
| echo "Cargo version $current_cargo_version does not match package version $current_version" | |
| exit 1 | |
| fi | |
| { | |
| echo "currentVersion=$current_version" | |
| echo "version=$expected_version" | |
| echo "tag=$tag" | |
| echo "branch=$branch" | |
| echo "title=chore: ${expected_version} 수동 bump" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Apply version bump | |
| env: | |
| RELEASE_TAG: ${{ steps.meta.outputs.tag }} | |
| run: node ./scripts/bump-release-version.mjs "$RELEASE_TAG" | |
| - name: Verify changed files before validation | |
| shell: bash | |
| run: | | |
| # shellcheck disable=SC2016 | |
| node --input-type=module -e ' | |
| import { execFileSync } from "node:child_process"; | |
| import { versionFilePaths } from "./scripts/bump-release-version.mjs"; | |
| const changedFiles = execFileSync("git", ["diff", "--name-only"], { encoding: "utf8" }) | |
| .trim() | |
| .split("\n") | |
| .filter(Boolean) | |
| .sort(); | |
| const expectedFiles = [...versionFilePaths].sort(); | |
| if (JSON.stringify(changedFiles) !== JSON.stringify(expectedFiles)) { | |
| throw new Error(`Unexpected changed files: ${changedFiles.join(", ")}`); | |
| } | |
| ' | |
| - name: Run release contract tests | |
| run: npm run test:release-contract | |
| - name: Run Rust workspace tests | |
| run: npm test | |
| - name: Verify changed files after validation | |
| shell: bash | |
| run: | | |
| # shellcheck disable=SC2016 | |
| node --input-type=module -e ' | |
| import { execFileSync } from "node:child_process"; | |
| import { validatedVersionFilePaths } from "./scripts/bump-release-version.mjs"; | |
| const changedFiles = execFileSync("git", ["diff", "--name-only"], { encoding: "utf8" }) | |
| .trim() | |
| .split("\n") | |
| .filter(Boolean) | |
| .sort(); | |
| const expectedFiles = [...validatedVersionFilePaths].sort(); | |
| if (JSON.stringify(changedFiles) !== JSON.stringify(expectedFiles)) { | |
| throw new Error(`Unexpected changed files after validation: ${changedFiles.join(", ")}`); | |
| } | |
| ' | |
| - name: Stop after dry run | |
| if: ${{ inputs.dry_run }} | |
| run: echo "Dry run requested; skipping branch push and PR creation." | |
| - name: Configure git author | |
| if: ${{ !inputs.dry_run }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| - name: Ensure skip-changelog label exists | |
| if: ${{ !inputs.dry_run }} | |
| shell: bash | |
| run: | | |
| if gh label list --search "skip-changelog" --json name --jq 'map(select(.name == "skip-changelog")) | length > 0' | grep -q true; then | |
| exit 0 | |
| fi | |
| gh label create "skip-changelog" \ | |
| --color "6E7781" \ | |
| --description "Exclude automated release prep PRs from release notes." | |
| - name: Resolve existing open PR | |
| if: ${{ !inputs.dry_run }} | |
| id: existing_pr | |
| shell: bash | |
| env: | |
| BUMP_BRANCH: ${{ steps.meta.outputs.branch }} | |
| EXPECTED_VERSION: ${{ steps.meta.outputs.version }} | |
| run: | | |
| pr_json=$(gh pr list \ | |
| --head "$BUMP_BRANCH" \ | |
| --state open \ | |
| --json number,url,baseRefName,labels) | |
| pr_number=$(printf '%s' "$pr_json" | jq -r '.[0].number // empty') | |
| pr_url=$(printf '%s' "$pr_json" | jq -r '.[0].url // empty') | |
| pr_base=$(printf '%s' "$pr_json" | jq -r '.[0].baseRefName // empty') | |
| has_skip_changelog=$(printf '%s' "$pr_json" | jq -r 'if (.[0].labels // []) | map(.name) | index("skip-changelog") then "true" else "false" end') | |
| if [ -n "$pr_number" ] && [ "$pr_base" != "master" ]; then | |
| echo "Existing PR $pr_url targets base '$pr_base', expected 'master'." | |
| exit 1 | |
| fi | |
| if [ -n "$pr_number" ]; then | |
| git fetch origin \ | |
| "refs/heads/master:refs/remotes/origin/master" \ | |
| "refs/heads/${BUMP_BRANCH}:refs/remotes/origin/${BUMP_BRANCH}" | |
| # shellcheck disable=SC2016 | |
| node --input-type=module -e ' | |
| import { execFileSync } from "node:child_process"; | |
| import { validatedVersionFilePaths } from "./scripts/bump-release-version.mjs"; | |
| const branch = process.argv[1]; | |
| const expectedVersion = process.argv[2]; | |
| const diffFiles = execFileSync( | |
| "git", | |
| ["diff", "--name-only", `refs/remotes/origin/master...refs/remotes/origin/${branch}`], | |
| { encoding: "utf8" }, | |
| ) | |
| .trim() | |
| .split("\n") | |
| .filter(Boolean) | |
| .sort(); | |
| const expectedFiles = [...validatedVersionFilePaths].sort(); | |
| if (JSON.stringify(diffFiles) !== JSON.stringify(expectedFiles)) { | |
| throw new Error(`Existing PR is not a version-only bump: ${diffFiles.join(", ")}`); | |
| } | |
| const packageManifest = JSON.parse( | |
| execFileSync("git", ["show", `refs/remotes/origin/${branch}:package.json`], { | |
| encoding: "utf8", | |
| }), | |
| ); | |
| const cargoManifest = execFileSync( | |
| "git", | |
| ["show", `refs/remotes/origin/${branch}:crates/legolas-cli/Cargo.toml`], | |
| { encoding: "utf8" }, | |
| ); | |
| const cargoMatch = cargoManifest.match(/^version\s*=\s*"([^"]+)"$/m); | |
| if (packageManifest.version !== expectedVersion) { | |
| throw new Error(`package.json version mismatch: ${packageManifest.version}`); | |
| } | |
| if (!cargoMatch?.[1]) { | |
| throw new Error("Unable to read crates/legolas-cli/Cargo.toml version from existing PR"); | |
| } | |
| if (cargoMatch[1] !== expectedVersion) { | |
| throw new Error(`crates/legolas-cli/Cargo.toml version mismatch: ${cargoMatch[1]}`); | |
| } | |
| ' "$BUMP_BRANCH" "$EXPECTED_VERSION" | |
| fi | |
| if [ -n "$pr_number" ] && [ "$has_skip_changelog" != "true" ]; then | |
| gh pr edit "$pr_number" --add-label skip-changelog | |
| fi | |
| { | |
| echo "number=$pr_number" | |
| echo "url=$pr_url" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Prepare bump branch | |
| if: ${{ !inputs.dry_run && steps.existing_pr.outputs.number == '' }} | |
| shell: bash | |
| env: | |
| BUMP_BRANCH: ${{ steps.meta.outputs.branch }} | |
| BUMP_TITLE: ${{ steps.meta.outputs.title }} | |
| run: | | |
| lease_args=() | |
| if git ls-remote --exit-code --heads origin "$BUMP_BRANCH" >/dev/null 2>&1; then | |
| echo "Remote branch $BUMP_BRANCH already exists; refreshing it with the verified bump commit." | |
| git fetch origin "refs/heads/${BUMP_BRANCH}:refs/remotes/origin/${BUMP_BRANCH}" | |
| lease_args+=(--force-with-lease="refs/heads/${BUMP_BRANCH}:$(git rev-parse "refs/remotes/origin/${BUMP_BRANCH}")") | |
| fi | |
| git switch -C "$BUMP_BRANCH" | |
| git add package.json crates/legolas-cli/Cargo.toml Cargo.lock | |
| git commit -m "$BUMP_TITLE" | |
| git push "${lease_args[@]}" -u origin "$BUMP_BRANCH" | |
| - name: Dispatch CI for bump branch | |
| if: ${{ !inputs.dry_run }} | |
| shell: bash | |
| env: | |
| BUMP_BRANCH: ${{ steps.meta.outputs.branch }} | |
| run: gh workflow run ci.yml --ref "$BUMP_BRANCH" | |
| - name: Resolve release candidate target | |
| if: ${{ !inputs.dry_run }} | |
| id: candidate_target | |
| shell: bash | |
| env: | |
| BUMP_BRANCH: ${{ steps.meta.outputs.branch }} | |
| EXISTING_PR_NUMBER: ${{ steps.existing_pr.outputs.number }} | |
| run: | | |
| if [ -n "$EXISTING_PR_NUMBER" ]; then | |
| git fetch origin "refs/heads/${BUMP_BRANCH}:refs/remotes/origin/${BUMP_BRANCH}" | |
| target_sha="$(git rev-parse "refs/remotes/origin/${BUMP_BRANCH}")" | |
| else | |
| target_sha="$(git rev-parse HEAD)" | |
| fi | |
| echo "sha=$target_sha" >> "$GITHUB_OUTPUT" | |
| - name: Dispatch release candidate verification | |
| if: ${{ !inputs.dry_run }} | |
| shell: bash | |
| env: | |
| CANDIDATE_SHA: ${{ steps.candidate_target.outputs.sha }} | |
| run: gh workflow run release-candidate.yml --ref master -f target_sha="$CANDIDATE_SHA" | |
| - name: Create or reuse draft PR | |
| if: ${{ !inputs.dry_run }} | |
| shell: bash | |
| env: | |
| BUMP_BRANCH: ${{ steps.meta.outputs.branch }} | |
| BUMP_TITLE: ${{ steps.meta.outputs.title }} | |
| CURRENT_VERSION: ${{ steps.meta.outputs.currentVersion }} | |
| DEFAULT_GH_TOKEN: ${{ github.token }} | |
| EXISTING_PR_URL: ${{ steps.existing_pr.outputs.url }} | |
| PR_CREATE_TOKEN: ${{ secrets.LEGOLAS_RELEASE_BOT_TOKEN }} | |
| RELEASE_TAG: ${{ steps.meta.outputs.tag }} | |
| RELEASE_VERSION: ${{ steps.meta.outputs.version }} | |
| run: | | |
| printf '%s\n' \ | |
| "## 배경 / Background" \ | |
| "" \ | |
| "- manual release bump workflow로 \`${CURRENT_VERSION}\`에서 \`${RELEASE_VERSION}\`으로 승격합니다." \ | |
| "- 실제 tag / publish는 이 PR merge 이후 별도 \`release.yml\` workflow에서 진행합니다." \ | |
| "" \ | |
| "## 변경 사항 / Changes" \ | |
| "" \ | |
| "- \`package.json\`, \`crates/legolas-cli/Cargo.toml\`, \`Cargo.lock\` version을 \`${RELEASE_VERSION}\`으로 맞췄습니다." \ | |
| "" \ | |
| "## 검증 / Verification" \ | |
| "" \ | |
| "- \`node ./scripts/bump-release-version.mjs ${RELEASE_TAG}\`" \ | |
| "- \`npm run test:release-contract\`" \ | |
| "- \`npm test\`" \ | |
| "- bump branch에 대해 \`legolas-ci\` 와 \`Release Candidate Core Verification\` workflow를 dispatch했습니다." \ | |
| "" \ | |
| "## 리스크 / Risks" \ | |
| "" \ | |
| "- 실제 tag / publish는 아직 실행하지 않았습니다." \ | |
| "- merge 후 같은 commit의 candidate verification과 CI를 확인한 뒤 tag를 생성해야 합니다." \ | |
| > pr-body.md | |
| if [ -n "$PR_CREATE_TOKEN" ]; then | |
| export GH_TOKEN="$PR_CREATE_TOKEN" | |
| token_source="LEGOLAS_RELEASE_BOT_TOKEN" | |
| else | |
| export GH_TOKEN="$DEFAULT_GH_TOKEN" | |
| token_source="github.token" | |
| fi | |
| if [ -n "$EXISTING_PR_URL" ]; then | |
| echo "Reusing existing PR: $EXISTING_PR_URL" | |
| exit 0 | |
| fi | |
| if pr_url=$(gh pr create \ | |
| --draft \ | |
| --base master \ | |
| --head "$BUMP_BRANCH" \ | |
| --title "$BUMP_TITLE" \ | |
| --body-file pr-body.md \ | |
| --label skip-changelog 2>/tmp/legolas-gh-pr-create-error.txt); then | |
| echo "Created draft PR: $pr_url" | |
| exit 0 | |
| fi | |
| compare_url="https://github.com/${GITHUB_REPOSITORY}/compare/master...${BUMP_BRANCH}?expand=1" | |
| { | |
| echo "### Manual release bump failed to create a PR" | |
| echo | |
| echo "The bump branch was pushed, but the workflow could not open a draft PR." | |
| echo | |
| echo "- branch: \`${BUMP_BRANCH}\`" | |
| echo "- title: \`${BUMP_TITLE}\`" | |
| echo "- token source: \`$token_source\`" | |
| echo "- compare URL: $compare_url" | |
| echo | |
| echo "Either enable repository Actions permission to create pull requests, or configure \`LEGOLAS_RELEASE_BOT_TOKEN\` with pull request creation permission." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| cat /tmp/legolas-gh-pr-create-error.txt | |
| echo "::error::Unable to create a draft PR for $BUMP_BRANCH. See the step summary for required repository or token changes." | |
| exit 1 |