earthtojake--text-to-cad
513 行
20 KiB
YAML
513 行
20 KiB
YAML
name: Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
bump:
|
|
description: Semver part to bump when set_version is empty.
|
|
required: true
|
|
default: patch
|
|
type: choice
|
|
options:
|
|
- patch
|
|
- minor
|
|
- major
|
|
set_version:
|
|
description: Exact X.Y.Z version to release instead of bumping. Set to the current version to resume a failed publish.
|
|
required: false
|
|
type: string
|
|
base_branch:
|
|
description: Branch to release from and merge the release PR into.
|
|
required: true
|
|
default: develop
|
|
type: string
|
|
target_branch:
|
|
description: Publish target. Use main for releases; use build-test only to test CI/CD or build changes.
|
|
required: true
|
|
default: main
|
|
type: choice
|
|
options:
|
|
- main
|
|
- build-test
|
|
dry_run:
|
|
description: Show the release version changes without pushing, merging, or publishing.
|
|
required: true
|
|
default: false
|
|
type: boolean
|
|
publish:
|
|
description: Publish the GitHub Release. Set to false to leave it as a draft.
|
|
required: true
|
|
default: true
|
|
type: boolean
|
|
create_release:
|
|
description: Create the GitHub Release after pushing the tag.
|
|
required: true
|
|
default: true
|
|
type: boolean
|
|
auto_merge:
|
|
description: Merge the release PR immediately, then publish. Set to false to only prepare the PR.
|
|
required: true
|
|
default: true
|
|
type: boolean
|
|
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
|
|
concurrency:
|
|
group: release-${{ inputs.base_branch }}-${{ inputs.target_branch }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
release-pr:
|
|
name: Release PR
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
version: ${{ steps.version.outputs.version }}
|
|
source_sha: ${{ steps.publish_source.outputs.sha }}
|
|
|
|
steps:
|
|
- name: Check out base branch
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ inputs.base_branch }}
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
|
|
- name: Configure release credentials
|
|
env:
|
|
RELEASE_ORCHESTRATOR_TOKEN: ${{ secrets.RELEASE_ORCHESTRATOR_TOKEN }}
|
|
PREPARE_RELEASE_TOKEN: ${{ secrets.PREPARE_RELEASE_TOKEN }}
|
|
PUBLISH_PUSH_TOKEN: ${{ secrets.PUBLISH_PUSH_TOKEN }}
|
|
BUILD_TEST_PUSH_TOKEN: ${{ secrets.BUILD_TEST_PUSH_TOKEN }}
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: |
|
|
release_token="${RELEASE_ORCHESTRATOR_TOKEN:-${PREPARE_RELEASE_TOKEN:-${PUBLISH_PUSH_TOKEN:-${BUILD_TEST_PUSH_TOKEN:-$GITHUB_TOKEN}}}}"
|
|
if [ -n "${RELEASE_ORCHESTRATOR_TOKEN:-}" ]; then
|
|
echo "Using RELEASE_ORCHESTRATOR_TOKEN for release orchestration."
|
|
elif [ -n "${PREPARE_RELEASE_TOKEN:-}" ]; then
|
|
echo "Using PREPARE_RELEASE_TOKEN for release orchestration."
|
|
elif [ -n "${PUBLISH_PUSH_TOKEN:-}" ]; then
|
|
echo "Using PUBLISH_PUSH_TOKEN for release orchestration."
|
|
elif [ -n "${BUILD_TEST_PUSH_TOKEN:-}" ]; then
|
|
echo "Using BUILD_TEST_PUSH_TOKEN for release orchestration."
|
|
else
|
|
echo "Using GITHUB_TOKEN for release orchestration."
|
|
fi
|
|
|
|
git remote set-url origin "https://x-access-token:${release_token}@github.com/${GITHUB_REPOSITORY}.git"
|
|
{
|
|
echo "GH_TOKEN=$release_token"
|
|
echo "RELEASE_TOKEN=$release_token"
|
|
} >> "$GITHUB_ENV"
|
|
|
|
- name: Prepare canonical version
|
|
id: version
|
|
env:
|
|
BUMP: ${{ inputs.bump }}
|
|
SET_VERSION: ${{ inputs.set_version }}
|
|
run: |
|
|
if [ -n "$SET_VERSION" ]; then
|
|
scripts/release/bump-version.sh --set-version "$SET_VERSION" --no-commit
|
|
else
|
|
scripts/release/bump-version.sh "$BUMP" --no-commit
|
|
fi
|
|
node scripts/release/sync-version.mjs
|
|
version="$(tr -d '[:space:]' < plugins/cad/VERSION)"
|
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
if git diff --quiet; then
|
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
echo "Base branch already contains release version $version; skipping the release PR."
|
|
else
|
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
echo "Prepared release version: $version"
|
|
fi
|
|
|
|
- name: Check release version metadata
|
|
env:
|
|
CHANGED: ${{ steps.version.outputs.changed }}
|
|
run: |
|
|
node scripts/release/sync-version.mjs --check
|
|
if [ "$CHANGED" = "true" ]; then
|
|
scripts/release/check-version.sh --incremented-from origin/main
|
|
latest_tag="$(git tag --list '[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -n 1 || true)"
|
|
if [ -n "$latest_tag" ]; then
|
|
scripts/release/check-version.sh --incremented-from "refs/tags/$latest_tag"
|
|
fi
|
|
else
|
|
scripts/release/check-version.sh
|
|
echo "No version metadata changes; the publish gate decides whether this version still needs to ship."
|
|
fi
|
|
|
|
- name: Check development symlink layout
|
|
run: scripts/dev/setup-symlinks.sh --check
|
|
|
|
- name: Show dry-run diff
|
|
if: inputs.dry_run
|
|
run: |
|
|
git diff --stat
|
|
git diff -- plugins/cad/VERSION
|
|
|
|
- name: Stop after dry run
|
|
if: inputs.dry_run
|
|
run: echo "Dry run requested; skipping release branch, PR merge, and publish."
|
|
|
|
- name: Create or update release pull request
|
|
if: ${{ !inputs.dry_run && steps.version.outputs.changed == 'true' }}
|
|
id: release_pr
|
|
env:
|
|
BASE_BRANCH: ${{ inputs.base_branch }}
|
|
VERSION: ${{ steps.version.outputs.version }}
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
|
|
branch="release/$VERSION"
|
|
body="Bumps plugins/cad/VERSION and derived package/plugin metadata to $VERSION. This PR was created by the one-button Release workflow, which merges it into $BASE_BRANCH immediately; the publish job revalidates the full production bundle before anything ships to the target branch."
|
|
|
|
git checkout -B "$branch"
|
|
git add -A
|
|
git commit -m "Release $VERSION"
|
|
git push --force-with-lease origin "$branch"
|
|
|
|
if pr_json="$(gh pr view "$branch" --repo "$GITHUB_REPOSITORY" --json number,url 2>/dev/null)"; then
|
|
pr_number="$(printf '%s\n' "$pr_json" | jq -r '.number')"
|
|
pr_url="$(printf '%s\n' "$pr_json" | jq -r '.url')"
|
|
gh pr edit "$pr_number" \
|
|
--repo "$GITHUB_REPOSITORY" \
|
|
--base "$BASE_BRANCH" \
|
|
--title "Release $VERSION" \
|
|
--body "$body"
|
|
else
|
|
pr_url="$(gh pr create \
|
|
--repo "$GITHUB_REPOSITORY" \
|
|
--base "$BASE_BRANCH" \
|
|
--head "$branch" \
|
|
--title "Release $VERSION" \
|
|
--body "$body")"
|
|
pr_number="$(gh pr view "$pr_url" --repo "$GITHUB_REPOSITORY" --json number --jq '.number')"
|
|
fi
|
|
|
|
echo "created=true" >> "$GITHUB_OUTPUT"
|
|
echo "number=$pr_number" >> "$GITHUB_OUTPUT"
|
|
echo "url=$pr_url" >> "$GITHUB_OUTPUT"
|
|
echo "Release PR: $pr_url"
|
|
|
|
- name: Merge release pull request
|
|
if: ${{ !inputs.dry_run && inputs.auto_merge && steps.release_pr.outputs.created == 'true' }}
|
|
env:
|
|
PR_NUMBER: ${{ steps.release_pr.outputs.number }}
|
|
run: |
|
|
pr_json="$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json headRefOid)"
|
|
head_sha="$(printf '%s\n' "$pr_json" | jq -r '.headRefOid')"
|
|
merged=false
|
|
for attempt in $(seq 1 6); do
|
|
if gh api \
|
|
--method PUT \
|
|
"repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/merge" \
|
|
-f merge_method=merge \
|
|
-f sha="$head_sha"; then
|
|
merged=true
|
|
break
|
|
fi
|
|
echo "Merge attempt $attempt failed; retrying while GitHub computes mergeability..."
|
|
sleep 10
|
|
done
|
|
if [ "$merged" != "true" ]; then
|
|
echo "Could not merge release PR #$PR_NUMBER." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Stop before publish when auto-merge is disabled
|
|
if: ${{ !inputs.dry_run && !inputs.auto_merge }}
|
|
run: echo "auto_merge=false; the release PR was prepared but the publish jobs were skipped."
|
|
|
|
- name: Resolve publish source
|
|
if: ${{ !inputs.dry_run && inputs.auto_merge }}
|
|
id: publish_source
|
|
env:
|
|
BASE_BRANCH: ${{ inputs.base_branch }}
|
|
run: |
|
|
git fetch --no-tags origin "+$BASE_BRANCH:refs/remotes/origin/$BASE_BRANCH"
|
|
source_sha="$(git rev-parse "origin/$BASE_BRANCH^{commit}")"
|
|
echo "sha=$source_sha" >> "$GITHUB_OUTPUT"
|
|
echo "Publishing $BASE_BRANCH at $source_sha."
|
|
|
|
publish:
|
|
name: Publish
|
|
needs: release-pr
|
|
if: ${{ !inputs.dry_run && inputs.auto_merge }}
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
published: ${{ steps.push.outputs.published }}
|
|
publish_sha: ${{ steps.commit.outputs.publish_sha }}
|
|
|
|
steps:
|
|
- name: Check out publish source
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ needs.release-pr.outputs.source_sha }}
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Configure publish credentials
|
|
env:
|
|
PUBLISH_PUSH_TOKEN: ${{ secrets.PUBLISH_PUSH_TOKEN }}
|
|
BUILD_TEST_PUSH_TOKEN: ${{ secrets.BUILD_TEST_PUSH_TOKEN }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
if [ -n "$PUBLISH_PUSH_TOKEN" ]; then
|
|
git remote set-url origin "https://x-access-token:${PUBLISH_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
|
echo "Using PUBLISH_PUSH_TOKEN for publish push."
|
|
elif [ -n "$BUILD_TEST_PUSH_TOKEN" ]; then
|
|
git remote set-url origin "https://x-access-token:${BUILD_TEST_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
|
echo "Using BUILD_TEST_PUSH_TOKEN for publish push."
|
|
else
|
|
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
|
echo "Using GITHUB_TOKEN for publish push."
|
|
fi
|
|
|
|
- name: Check canonical release version
|
|
run: scripts/release/check-version.sh
|
|
|
|
- name: Evaluate release gate
|
|
id: gate
|
|
env:
|
|
BASE_BRANCH: ${{ inputs.base_branch }}
|
|
TARGET_BRANCH: ${{ inputs.target_branch }}
|
|
run: |
|
|
source_sha="$(git rev-parse HEAD)"
|
|
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
|
|
|
|
if [ "$TARGET_BRANCH" != "main" ]; then
|
|
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
|
echo "Build-test publish requested for $source_sha (CI/CD or build testing only)."
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$BASE_BRANCH" != "develop" ]; then
|
|
echo "Real releases must publish develop to main; got base_branch=$BASE_BRANCH." >&2
|
|
exit 2
|
|
fi
|
|
|
|
current_version="$(tr -d '[:space:]' < plugins/cad/VERSION)"
|
|
latest_tag="$(git tag --list '[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -n 1 || true)"
|
|
if ! scripts/release/check-version.sh --incremented-from origin/main >/tmp/publish-main-version-check.log 2>&1; then
|
|
main_version="$(git show origin/main:plugins/cad/VERSION | tr -d '[:space:]')"
|
|
if [ "$main_version" = "$current_version" ] && ! git rev-parse --verify --quiet "refs/tags/$current_version" >/dev/null; then
|
|
echo "Release version already reached origin/main, but tag $current_version is missing; resuming publish."
|
|
else
|
|
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
|
echo "Skipping publish: version has not advanced past origin/main."
|
|
exit 0
|
|
fi
|
|
fi
|
|
if [ -n "$latest_tag" ] && ! scripts/release/check-version.sh --incremented-from "refs/tags/$latest_tag" >/tmp/publish-tag-version-check.log 2>&1; then
|
|
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
|
echo "Skipping publish: version has not advanced past latest tag $latest_tag."
|
|
exit 0
|
|
fi
|
|
|
|
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
|
echo "Release version advanced; publishing $source_sha to $TARGET_BRANCH."
|
|
|
|
- name: Skip non-release publish
|
|
if: steps.gate.outputs.should_publish != 'true'
|
|
run: echo "Publish only updates main when the source version is newer than main and the latest release tag."
|
|
|
|
- name: Validate publish target
|
|
if: steps.gate.outputs.should_publish == 'true'
|
|
env:
|
|
SOURCE_SHA: ${{ steps.gate.outputs.source_sha }}
|
|
TARGET_BRANCH: ${{ inputs.target_branch }}
|
|
run: |
|
|
scripts/release/check-publish-source.sh --source-ref "$SOURCE_SHA" --target-ref origin/main
|
|
if [ "$TARGET_BRANCH" = "main" ]; then
|
|
echo "Validated main publish target."
|
|
else
|
|
echo "Validated build-test target against origin/main."
|
|
fi
|
|
|
|
- name: Set up dependencies
|
|
if: steps.gate.outputs.should_publish == 'true'
|
|
uses: ./.github/actions/setup-deps
|
|
|
|
- name: Check development symlink layout
|
|
if: steps.gate.outputs.should_publish == 'true'
|
|
run: scripts/dev/setup-symlinks.sh --check
|
|
|
|
- name: Bundle production outputs
|
|
if: steps.gate.outputs.should_publish == 'true'
|
|
run: scripts/bundle/bundle.sh --clean
|
|
|
|
- name: Validate production bundle layout
|
|
if: steps.gate.outputs.should_publish == 'true'
|
|
run: scripts/github-workflows/check-builds.sh --skip-bundle-check
|
|
|
|
- name: Run documentation checks
|
|
if: steps.gate.outputs.should_publish == 'true'
|
|
run: scripts/test/test-docs.sh
|
|
|
|
- name: Run code tests
|
|
if: steps.gate.outputs.should_publish == 'true'
|
|
run: scripts/test/test.sh
|
|
|
|
- name: Remove models from publish tree
|
|
if: steps.gate.outputs.should_publish == 'true'
|
|
run: |
|
|
rm -rf models
|
|
if [ -e models ]; then
|
|
echo "Failed to remove models from the publish tree." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Commit publish result
|
|
if: steps.gate.outputs.should_publish == 'true'
|
|
id: commit
|
|
env:
|
|
BASE_BRANCH: ${{ inputs.base_branch }}
|
|
SOURCE_SHA: ${{ steps.gate.outputs.source_sha }}
|
|
TARGET_BRANCH: ${{ inputs.target_branch }}
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
git add -A
|
|
if [ -n "$(git ls-files -- models)" ]; then
|
|
echo "models/ must not be present in the publish commit." >&2
|
|
git ls-files -- models >&2
|
|
exit 1
|
|
fi
|
|
|
|
version="$(tr -d '[:space:]' < plugins/cad/VERSION)"
|
|
if target_base_sha="$(git rev-parse --verify "origin/$TARGET_BRANCH^{commit}" 2>/dev/null)"; then
|
|
echo "Using existing $TARGET_BRANCH as publish parent: $target_base_sha"
|
|
else
|
|
target_base_sha="$(git rev-parse --verify "origin/main^{commit}")"
|
|
echo "Target branch $TARGET_BRANCH does not exist; using origin/main as publish parent: $target_base_sha"
|
|
fi
|
|
release_base_sha="$(git rev-parse --verify "origin/main^{commit}")"
|
|
previous_source_sha="$(scripts/release/check-publish-source.sh --target-ref origin/main --print-previous-source)"
|
|
included_commits="$(git log --oneline --no-decorate "$previous_source_sha..$SOURCE_SHA" || true)"
|
|
|
|
message_file="$(mktemp)"
|
|
{
|
|
echo "Publish $version from $BASE_BRANCH to $TARGET_BRANCH"
|
|
echo
|
|
echo "Source ref: $BASE_BRANCH"
|
|
echo "Source commit: $SOURCE_SHA"
|
|
echo "Target branch: $TARGET_BRANCH"
|
|
echo "Previous target: $target_base_sha"
|
|
echo "Release base: $release_base_sha"
|
|
echo "Previous source: $previous_source_sha"
|
|
echo
|
|
echo "Included commits since previous source:"
|
|
if [ -n "$included_commits" ]; then
|
|
echo "$included_commits"
|
|
else
|
|
echo "(no source commits beyond previous published source)"
|
|
fi
|
|
} > "$message_file"
|
|
|
|
tree_sha="$(git write-tree)"
|
|
commit_parents=(-p "$target_base_sha")
|
|
if [ "$SOURCE_SHA" != "$target_base_sha" ]; then
|
|
commit_parents+=(-p "$SOURCE_SHA")
|
|
fi
|
|
publish_commit="$(git commit-tree "$tree_sha" "${commit_parents[@]}" -F "$message_file")"
|
|
git reset --hard "$publish_commit"
|
|
echo "publish_sha=$publish_commit" >> "$GITHUB_OUTPUT"
|
|
echo "Created publish commit: $publish_commit"
|
|
|
|
- name: Push target branch
|
|
if: steps.gate.outputs.should_publish == 'true'
|
|
id: push
|
|
env:
|
|
TARGET_BRANCH: ${{ inputs.target_branch }}
|
|
run: |
|
|
if [ "$TARGET_BRANCH" = "main" ]; then
|
|
git push origin HEAD:main
|
|
else
|
|
git push --force-with-lease origin "HEAD:$TARGET_BRANCH"
|
|
fi
|
|
echo "published=true" >> "$GITHUB_OUTPUT"
|
|
|
|
upload-models:
|
|
name: Upload Models
|
|
needs:
|
|
- release-pr
|
|
- publish
|
|
if: ${{ needs.publish.outputs.published == 'true' && inputs.target_branch == 'main' }}
|
|
uses: ./.github/workflows/upload-models.yml
|
|
with:
|
|
ref: ${{ needs.release-pr.outputs.source_sha }}
|
|
secrets: inherit
|
|
|
|
deploy-docs:
|
|
name: Deploy Docs
|
|
needs: publish
|
|
if: ${{ needs.publish.outputs.published == 'true' && inputs.target_branch == 'main' }}
|
|
uses: ./.github/workflows/deploy-docs.yml
|
|
with:
|
|
ref: ${{ needs.publish.outputs.publish_sha }}
|
|
secrets: inherit
|
|
|
|
deploy-viewer:
|
|
name: Deploy Viewer
|
|
needs: publish
|
|
if: ${{ needs.publish.outputs.published == 'true' && inputs.target_branch == 'main' }}
|
|
uses: ./.github/workflows/deploy-viewer.yml
|
|
with:
|
|
ref: ${{ needs.publish.outputs.publish_sha }}
|
|
secrets: inherit
|
|
|
|
tag-release:
|
|
name: Tag and GitHub Release
|
|
needs:
|
|
- publish
|
|
- upload-models
|
|
- deploy-docs
|
|
- deploy-viewer
|
|
if: ${{ needs.publish.outputs.published == 'true' && inputs.target_branch == 'main' }}
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Check out publish commit
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ needs.publish.outputs.publish_sha }}
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Configure tag push credentials
|
|
env:
|
|
PUBLISH_PUSH_TOKEN: ${{ secrets.PUBLISH_PUSH_TOKEN }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
if [ -n "$PUBLISH_PUSH_TOKEN" ]; then
|
|
git remote set-url origin "https://x-access-token:${PUBLISH_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
|
echo "Using PUBLISH_PUSH_TOKEN for tag push."
|
|
else
|
|
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
|
echo "Using GITHUB_TOKEN for tag push."
|
|
fi
|
|
|
|
- name: Create release tag and GitHub Release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
INPUT_PUBLISH: ${{ inputs.publish }}
|
|
INPUT_CREATE_RELEASE: ${{ inputs.create_release }}
|
|
run: |
|
|
args=(--target HEAD)
|
|
if [ "$INPUT_PUBLISH" = "true" ]; then
|
|
args+=(--publish)
|
|
fi
|
|
if [ "$INPUT_CREATE_RELEASE" != "true" ]; then
|
|
args+=(--skip-release)
|
|
fi
|
|
scripts/release/publish-github-release.sh "${args[@]}"
|