aviso-server 0.9.2

Notification service for data-driven workflows with live and replay APIs.
# Builds mdBook docs and publishes them to ECMWF Sites:
# - PRs get preview publish/unpublish under `aviso-server/pull-requests`.
# - Pushes to `main`/tags publish canonical docs under `aviso-server`.
# - Manual runs are build-only by default; publish is opt-in.
name: Docs Sites Publish

on:
  pull_request:
    types: [opened, synchronize, reopened, closed]
  push:
    branches:
      - main
    tags:
      - "*"
  workflow_dispatch:
    inputs:
      # Safe default: allow validating workflow/build from GitHub UI without publishing.
      publish:
        description: "Also publish docs to sites (default: build artifact only)"
        required: false
        type: boolean
        default: false
      # Optional override for manual publish id/path segment.
      publish_id:
        description: "Optional publish id when publish=true (default: manual-<run_id>)"
        required: false
        type: string

concurrency:
  # One run per PR or ref. cancel-in-progress is on for PRs only, so rapid PR
  # pushes drop their own stale rebuilds, while a push to main or a tag never
  # cancels an in-flight canonical publish (no half-uploaded site).
  group: docs-sites-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
  contents: read

jobs:
  # Build the publish artifact in the pinned CI image. The image bakes
  # mdbook + mdbook-mermaid, so their versions live in one place
  # (.github/ci/Dockerfile), not duplicated here. No sccache or S3 creds:
  # mdbook renders Markdown and runs the mermaid preprocessor, it compiles
  # no Rust, so there is nothing to cache.
  build-docs:
    # Trusted context only: the image is private and the runners are
    # self-hosted, so fork PRs cannot build here. Skip on PR close.
    if: >-
      ${{
        (github.event_name == 'push' || github.event_name == 'workflow_dispatch' ||
         (github.event_name == 'pull_request' && github.event.action != 'closed'))
        && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
      }}
    runs-on: [self-hosted, Linux, platform-builder-docker-xl, platform-builder-Ubuntu-22.04]
    permissions:
      contents: read
    container:
      # Pinned full tag, never :latest.
      image: eccr.ecmwf.int/aviso/server-ci:0.1.0
      credentials:
        username: ${{ secrets.ECMWF_DOCKER_REGISTRY_USERNAME }}
        password: ${{ secrets.ECMWF_DOCKER_REGISTRY_ACCESS_TOKEN }}
    outputs:
      artifact-id: ${{ steps.upload-docs.outputs.artifact-id }}
    steps:
      - uses: actions/checkout@v5

      - name: Build docs
        run: mdbook build docs

      - name: Archive documentation
        id: upload-docs
        uses: actions/upload-artifact@v7
        with:
          name: aviso-server-docs
          path: docs/book
          if-no-files-found: error
          overwrite: true

  # Publish PR preview docs and attach preview link to the PR.
  preview-publish:
    if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository }}
    needs: build-docs
    permissions:
      contents: read
      pull-requests: write
    uses: ecmwf/reusable-workflows/.github/workflows/pr-preview-publish.yml@5a1d1cb1442aa632f7823e4088d639b980627afc
    with:
      artifact-id: ${{ needs.build-docs.outputs.artifact-id }}
      space: docs
      name: aviso-server
      path: pull-requests
      link-tag: PREVIEW-PUBLISH-AVISO-DOCS
      link-text: "Docs Preview"
    secrets:
      sites-token: ${{ secrets.ECMWF_SITES_DOCS_AVISOSERVER_TOKEN }}

  # Remove preview docs when PR is closed to avoid stale preview content.
  preview-unpublish:
    if: ${{ github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository }}
    uses: ecmwf/reusable-workflows/.github/workflows/pr-preview-unpublish.yml@5a1d1cb1442aa632f7823e4088d639b980627afc
    with:
      space: docs
      name: aviso-server
      path: pull-requests
    secrets:
      sites-token: ${{ secrets.ECMWF_SITES_DOCS_AVISOSERVER_TOKEN }}

  # Compute a safe publish id used for docs publish path segments.
  # This prevents malformed or path-like values from workflow inputs/tags.
  compute-publish-id:
    if: >-
      ${{
        (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_type == 'tag')) ||
        (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true')
      }}
    runs-on: ubuntu-latest
    outputs:
      publish-id: ${{ steps.sanitize.outputs.publish_id }}
    steps:
      - name: Compute raw publish id
        id: raw
        # Refs and the manual input arrive through env, never inline ${{ }}
        # in the shell, so they cannot run as code.
        env:
          REF_NAME: ${{ github.ref_name }}
          EVENT_NAME: ${{ github.event_name }}
          INPUT_PUBLISH_ID: ${{ github.event.inputs.publish_id }}
          RUN_ID: ${{ github.run_id }}
        run: |
          set -euo pipefail
          RAW_ID="$REF_NAME"
          if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
            RAW_ID="$INPUT_PUBLISH_ID"
            if [ -z "$RAW_ID" ]; then
              RAW_ID="manual-$RUN_ID"
            fi
          fi
          # Random delimiter: a value containing a literal `EOF` line
          # cannot terminate the block early and inject extra outputs.
          DELIM="raw_id-$(uuidgen)"
          {
            echo "raw_id<<$DELIM"
            printf '%s\n' "$RAW_ID"
            echo "$DELIM"
          } >> "$GITHUB_OUTPUT"

      - name: Sanitize publish id
        id: sanitize
        env:
          RAW_ID: ${{ steps.raw.outputs.raw_id }}
        run: |
          set -euo pipefail
          # tr first: sed is line-oriented, so collapse CR/LF to a separator
          # up front, then keep only URL/path-safe chars and squeeze repeats.
          SANITIZED="$(printf '%s' "$RAW_ID" | tr '\r\n' '--' | sed -E 's|/+|-|g; s|[^A-Za-z0-9._-]|-|g; s|-+|-|g; s|\.+|.|g; s|^[.-]+||; s|[.-]+$||')"
          if [ -z "$SANITIZED" ]; then
            echo "::error::sanitized publish id is empty; refusing to publish"
            exit 1
          fi
          echo "publish_id=$SANITIZED" >> "$GITHUB_OUTPUT"

  # Publish canonical docs on:
  # - push to main/tags, or
  # - manual run when publish=true.
  publish-docs:
    if: >-
      ${{
        (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_type == 'tag')) ||
        (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true')
      }}
    needs:
      - build-docs
      - compute-publish-id
    uses: ecmwf/reusable-workflows/.github/workflows/docs-publish.yml@5a1d1cb1442aa632f7823e4088d639b980627afc
    with:
      artifact-id: ${{ needs.build-docs.outputs.artifact-id }}
      space: docs
      name: aviso-server
      path: ""
      # Use sanitized id from compute-publish-id.
      id: ${{ needs.compute-publish-id.outputs.publish-id }}
      # Keep stable aliases only for push events:
      # - tag publish => stable
      # - main publish => latest
      softlink: ${{ github.event_name == 'push' && (github.ref_type == 'tag' && 'stable' || github.ref_name == 'main' && 'latest') || '' }}
    secrets:
      sites-token: ${{ secrets.ECMWF_SITES_DOCS_AVISOSERVER_TOKEN }}