name: Version Bump on: push: branches: - dev paths-ignore: - 'VERSION' - '.github/workflows/version-bump.yml' jobs: bump-version: runs-on: ubuntu-latest timeout-minutes: 45 # Skip if commit message contains [skip ci] or [skip version] if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip version]') && !startsWith(github.event.head_commit.message, 'chore: bump version')" permissions: actions: read contents: write pull-requests: read statuses: write steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Get current version id: current run: | if [ -f VERSION ]; then VERSION=$(cat VERSION | tr -d '\n\r ') else VERSION="0.0.0" fi echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Current version: $VERSION" - name: Determine bump type id: bump env: # Pass the commit message through the environment, NOT string- # interpolated into the run script. Splicing # ${{ github.event.head_commit.message }} directly into the shell # source means a body containing backticks, $(...) or | is parsed # as code — which broke this job (commit e3337f0's body had # `vagrant up | Out-Host`, a syntax error) and is a command- # injection vector. As an env var it is inert text bash never re-parses. COMMIT_MSG: ${{ github.event.head_commit.message }} run: | # Major if a [major] tag OR a BREAKING/major: conventional prefix is # present; minor for [minor] tag OR feat/feature/minor: prefix; # otherwise patch. Supporting both keeps existing conventional # commits working while allowing explicit [minor]/[major] tags. if echo "$COMMIT_MSG" | grep -qiE '\[major\]' || \ echo "$COMMIT_MSG" | grep -qiE '^(BREAKING|breaking!|major):'; then echo "type=major" >> $GITHUB_OUTPUT echo "Bump type: major" elif echo "$COMMIT_MSG" | grep -qiE '\[minor\]' || \ echo "$COMMIT_MSG" | grep -qiE '^(feat|feature|minor):'; then echo "type=minor" >> $GITHUB_OUTPUT echo "Bump type: minor" else echo "type=patch" >> $GITHUB_OUTPUT echo "Bump type: patch" fi - name: Calculate new version id: new run: | VERSION="${{ steps.current.outputs.version }}" BUMP_TYPE="${{ steps.bump.outputs.type }}" # Parse version components MAJOR=$(echo $VERSION | cut -d. -f1) MINOR=$(echo $VERSION | cut -d. -f2) PATCH=$(echo $VERSION | cut -d. -f3) # Bump appropriate component case $BUMP_TYPE in major) MAJOR=$((MAJOR + 1)) MINOR=0 PATCH=0 ;; minor) MINOR=$((MINOR + 1)) PATCH=0 ;; patch) PATCH=$((PATCH + 1)) ;; esac NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT echo "New version: $NEW_VERSION" # A GITHUB_TOKEN push deliberately does not start another workflow run. # Advancing dev immediately therefore used to replace the tested PR head # with an untested VERSION-only commit. Wait for every sibling run on the # triggering SHA first; on a red commit, do not advance dev at all. - name: Wait for sibling CI before advancing dev env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | deadline=$((SECONDS + 2400)) settled_polls=0 while [ "$SECONDS" -lt "$deadline" ]; do runs_file="$RUNNER_TEMP/serverkit-sibling-runs.json" gh run list \ --repo "$GITHUB_REPOSITORY" \ --commit "$GITHUB_SHA" \ --limit 100 \ --json workflowName,event,status,conclusion,url \ > "$runs_file" relevant=$(jq '[.[] | select( .workflowName != "Version Bump" and .workflowName != "Discord Notify" )]' "$runs_file") failed=$(jq -r '.[] | select( .status == "completed" and .conclusion != "success" and .conclusion != "skipped" and .conclusion != "neutral" ) | "\(.workflowName) [\(.event)]: \(.conclusion) — \(.url)"' \ <<< "$relevant") if [ -n "$failed" ]; then echo "::error::Sibling CI failed; VERSION will not advance." printf '%s\n' "$failed" exit 1 fi pending=$(jq '[.[] | select(.status != "completed")] | length' \ <<< "$relevant") total=$(jq 'length' <<< "$relevant") echo "Sibling CI: $total discovered, $pending still running" if [ "$pending" -eq 0 ]; then settled_polls=$((settled_polls + 1)) # Three quiet polls prevent a just-created push/PR run from being # missed before GitHub has indexed it for `gh run list`. if [ "$settled_polls" -ge 3 ]; then break fi else settled_polls=0 fi sleep 10 done if [ "$SECONDS" -ge "$deadline" ]; then echo "::error::Timed out waiting for sibling CI; VERSION will not advance." exit 1 fi - name: Update VERSION file run: | echo "${{ steps.new.outputs.version }}" > VERSION cat VERSION - name: Commit and push id: commit run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add VERSION git commit -m "chore: bump version to ${{ steps.new.outputs.version }} [skip ci]" git push echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" # The bot push cannot fire Main Promotion either. Publish the exact # required context only after sibling CI passed and an open same-repo # dev -> main promotion was verified. This keeps strict branch protection # attached to the generated head instead of the now-parent commit. - name: Preserve the promotion gate on the generated commit env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NEW_SHA: ${{ steps.commit.outputs.sha }} run: | owner="${GITHUB_REPOSITORY%%/*}" pr_number=$(gh api --method GET "repos/$GITHUB_REPOSITORY/pulls" \ -f state=open \ -f base=main \ -f head="$owner:dev" \ --jq '.[0].number // empty') if [ -z "$pr_number" ]; then echo "No open dev -> main promotion; no promotion status is needed." exit 0 fi gh api --method POST "repos/$GITHUB_REPOSITORY/statuses/$NEW_SHA" \ -f state=success \ -f context='Require dev source' \ -f description='Sibling CI passed; verified same-repository dev -> main promotion' \ -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" echo "Published Require dev source for PR #$pr_number at $NEW_SHA" - name: Create summary env: # Same injection-safety reason as the bump step: keep the raw # commit message out of the run script source. TRIGGER_MSG: ${{ github.event.head_commit.message }} run: | echo "## Version Bump Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY echo "| Previous Version | ${{ steps.current.outputs.version }} |" >> $GITHUB_STEP_SUMMARY echo "| New Version | ${{ steps.new.outputs.version }} |" >> $GITHUB_STEP_SUMMARY echo "| Bump Type | ${{ steps.bump.outputs.type }} |" >> $GITHUB_STEP_SUMMARY # First line only, and via env — keeps a multi-line / special-char # body from corrupting the markdown table or the script. echo "| Trigger Commit | $(echo "$TRIGGER_MSG" | head -n1) |" >> $GITHUB_STEP_SUMMARY