Harden canary workflow: HEAD guards, ordering, notes escaping #1
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: Canary releases | |
| on: | |
| push: | |
| branches: | |
| - main | |
| permissions: | |
| contents: write | |
| packages: write | |
| concurrency: | |
| group: canary-main | |
| cancel-in-progress: true | |
| jobs: | |
| release-canary: | |
| name: Publish canary artifacts | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout source code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Guard - workflow SHA must match main HEAD | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| main_head=$(gh api "repos/${REPO}/git/ref/heads/main" --jq .object.sha) | |
| if [[ "$main_head" != "$SHA" ]]; then | |
| echo "::error::main HEAD ($main_head) differs from workflow SHA ($SHA); refusing to publish canary" | |
| exit 1 | |
| fi | |
| - name: Setup project and build environment | |
| uses: ./.github/actions/common-setup | |
| - name: Package application | |
| run: ./gradlew installDist | |
| - name: Create ZIP file from the directory | |
| run: zip -r octi-server-canary.zip ./build/install/octi-server | |
| - name: Prepare canary metadata | |
| id: canary-meta | |
| env: | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| short_sha="${SHA:0:7}" | |
| tag="canary" | |
| title="Canary (${short_sha})" | |
| notes_file="canary-notes.md" | |
| { | |
| printf '## Octi Server Canary\n\n' | |
| printf 'This is a rolling bleeding-edge pre-release built from `main`.\n\n' | |
| printf -- '- Commit: `%s`\n' "$SHA" | |
| printf -- '- Docker tags: `ghcr.io/%s:canary`, `ghcr.io/%s:sha-%s`\n\n' "$REPO" "$REPO" "$short_sha" | |
| printf 'Do not use this build for production unless you accept breakage risk.\n' | |
| } > "$notes_file" | |
| echo "tag=$tag" >> "$GITHUB_OUTPUT" | |
| echo "title=$title" >> "$GITHUB_OUTPUT" | |
| echo "notes_file=$notes_file" >> "$GITHUB_OUTPUT" | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a #v4.0.0 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd #v4.0.0 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 #v4.1.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate Docker metadata | |
| id: docker-meta | |
| uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf #v6.0.0 | |
| with: | |
| images: ghcr.io/${{ github.repository }} | |
| tags: | | |
| type=raw,value=canary | |
| type=sha,format=short,prefix=sha- | |
| labels: | | |
| org.opencontainers.image.title=Octi Server Canary | |
| org.opencontainers.image.description=Bleeding edge synchronization server for Octi | |
| org.opencontainers.image.vendor=d4rken-org | |
| org.opencontainers.image.version=canary | |
| env: | |
| DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index | |
| - name: Guard - main HEAD must still match before publishing | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| main_head=$(gh api "repos/${REPO}/git/ref/heads/main" --jq .object.sha) | |
| if [[ "$main_head" != "$SHA" ]]; then | |
| echo "::error::main HEAD ($main_head) advanced past workflow SHA ($SHA); aborting before Docker push" | |
| exit 1 | |
| fi | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 #v7.0.0 | |
| with: | |
| context: . | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.docker-meta.outputs.tags }} | |
| labels: ${{ steps.docker-meta.outputs.labels }} | |
| annotations: ${{ steps.docker-meta.outputs.annotations }} | |
| # The `canary` tag is intentionally mutable. Per .claude/rules/release.md, tag rulesets | |
| # in this org cover only `main` and `v*` tags, so GITHUB_TOKEN can force-update it. | |
| # If a future ruleset covers `canary*`, switch this step to an App token. | |
| - name: Move canary tag to current commit | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ github.sha }} | |
| TAG: ${{ steps.canary-meta.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| if gh api "repos/${REPO}/git/ref/tags/${TAG}" >/dev/null 2>&1; then | |
| gh api --method PATCH "repos/${REPO}/git/refs/tags/${TAG}" -f sha="$SHA" -F force=true >/dev/null | |
| else | |
| gh api --method POST "repos/${REPO}/git/refs" -f ref="refs/tags/${TAG}" -f sha="$SHA" >/dev/null | |
| fi | |
| - name: Update rolling canary pre-release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ steps.canary-meta.outputs.tag }} | |
| TITLE: ${{ steps.canary-meta.outputs.title }} | |
| NOTES_FILE: ${{ steps.canary-meta.outputs.notes_file }} | |
| run: | | |
| set -euo pipefail | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| gh release upload "$TAG" octi-server-canary.zip --clobber | |
| gh release edit "$TAG" --title "$TITLE" --notes-file "$NOTES_FILE" --prerelease --target "$TAG" | |
| else | |
| gh release create "$TAG" octi-server-canary.zip --title "$TITLE" --notes-file "$NOTES_FILE" --prerelease --target "$TAG" | |
| fi |