Publish verified Boatstack release #489
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
| # Boatstack-owned control plane. | |
| name: Publish verified Boatstack release | |
| on: | |
| schedule: | |
| - cron: "0 2 * * *" | |
| workflow_dispatch: | |
| permissions: | |
| actions: read | |
| contents: read | |
| concurrency: | |
| group: auto-release-boatstack | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Verify exact current main and CI | |
| id: source | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_SOURCE: ${{ github.sha }} | |
| shell: bash | |
| run: | | |
| [[ "$GITHUB_REF" == "refs/heads/main" ]] || { | |
| echo "BLOCKED: stable releases must run from main, not $GITHUB_REF." >&2 | |
| exit 2 | |
| } | |
| checked_out="$(git rev-parse HEAD)" | |
| [[ "$checked_out" == "$RELEASE_SOURCE" ]] || { | |
| echo "BLOCKED: checked-out source $checked_out does not match $RELEASE_SOURCE." >&2 | |
| exit 2 | |
| } | |
| remote_main="$(git ls-remote origin refs/heads/main | awk 'NR == 1 {print $1}')" | |
| [[ -n "$remote_main" && "$remote_main" == "$RELEASE_SOURCE" ]] || { | |
| echo "BLOCKED: main moved from $RELEASE_SOURCE to ${remote_main:-unknown}." >&2 | |
| exit 2 | |
| } | |
| verified_sha="$(gh run list \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --workflow .github/workflows/ci.yml \ | |
| --branch main \ | |
| --event push \ | |
| --commit "$RELEASE_SOURCE" \ | |
| --status success \ | |
| --limit 1 \ | |
| --json headSha \ | |
| --jq '.[0].headSha // ""')" | |
| [[ "$verified_sha" == "$RELEASE_SOURCE" ]] || { | |
| echo "BLOCKED: exact source $RELEASE_SOURCE has no successful main push CI run." >&2 | |
| exit 2 | |
| } | |
| echo "sha=$RELEASE_SOURCE" >> "$GITHUB_OUTPUT" | |
| - name: Detect pending release-bearing changes | |
| id: classify | |
| env: | |
| RELEASE_SOURCE: ${{ steps.source.outputs.sha }} | |
| run: >- | |
| python3 .github/scripts/release_candidate.py | |
| --repo . | |
| --source "$RELEASE_SOURCE" | |
| --github-output "$GITHUB_OUTPUT" | |
| - name: Report current release state | |
| if: steps.classify.outputs.release_required != 'true' | |
| run: echo "Boatstack has no verified unreleased changes; no release was created." | |
| - name: Create repository automation token | |
| if: steps.classify.outputs.release_required == 'true' | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| client-id: ${{ vars.BOATSTACK_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.BOATSTACK_APP_PRIVATE_KEY }} | |
| owner: operatorstack | |
| repositories: boatstack | |
| permission-contents: write | |
| - uses: actions/checkout@v7 | |
| if: steps.classify.outputs.release_required == 'true' | |
| with: | |
| ref: ${{ steps.source.outputs.sha }} | |
| fetch-depth: 0 | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Create next verified patch tag | |
| if: steps.classify.outputs.release_required == 'true' | |
| env: | |
| APP_SLUG: ${{ steps.app-token.outputs.app-slug }} | |
| EXPECTED_LATEST_TAG: ${{ steps.classify.outputs.latest_tag }} | |
| EXPECTED_NEXT_TAG: ${{ steps.classify.outputs.next_tag }} | |
| RELEASE_SOURCE: ${{ steps.source.outputs.sha }} | |
| shell: bash | |
| run: | | |
| checked_out="$(git rev-parse HEAD)" | |
| [[ "$checked_out" == "$RELEASE_SOURCE" ]] || { | |
| echo "BLOCKED: checked-out source changed before publication." >&2 | |
| exit 2 | |
| } | |
| remote_main="$(git ls-remote origin refs/heads/main | awk 'NR == 1 {print $1}')" | |
| [[ -n "$remote_main" && "$remote_main" == "$RELEASE_SOURCE" ]] || { | |
| echo "BLOCKED: main moved before publication; retry against the new head." >&2 | |
| exit 2 | |
| } | |
| git fetch --force --tags origin | |
| candidate="$(python3 .github/scripts/release_candidate.py --repo . --source "$RELEASE_SOURCE")" | |
| release_required="$(jq -r .release_required <<< "$candidate")" | |
| latest_tag="$(jq -r .latest_tag <<< "$candidate")" | |
| next_tag="$(jq -r .next_tag <<< "$candidate")" | |
| [[ "$release_required" == true ]] || { | |
| echo "BLOCKED: no unreleased change remains after refreshing tags." >&2 | |
| exit 2 | |
| } | |
| [[ "$latest_tag" == "$EXPECTED_LATEST_TAG" && "$next_tag" == "$EXPECTED_NEXT_TAG" ]] || { | |
| echo "BLOCKED: stable release tags changed during this run." >&2 | |
| exit 2 | |
| } | |
| if git ls-remote --exit-code --tags origin "refs/tags/$next_tag" >/dev/null 2>&1; then | |
| echo "BLOCKED: tag already exists: $next_tag" >&2 | |
| exit 2 | |
| fi | |
| git config user.name "${APP_SLUG}[bot]" | |
| git config user.email "${APP_SLUG}[bot]@users.noreply.github.com" | |
| git tag -a "$next_tag" -m "Boatstack $next_tag" "$RELEASE_SOURCE" | |
| git push origin "refs/tags/$next_tag" | |
| echo "Published verified release tag $next_tag from $RELEASE_SOURCE." |