# cratly — build a scavold/VitePress site
#
# Canonical URL:  https://cratly.io/ci/v1/build.yml
# Documentation:  https://cratly.io/reference/pipeline
# Licence:        MIT
#
# Include it from any GitLab instance — this file is served as a plain static
# file, so it needs neither an account on gitlab.com nor access to a registry:
#
#   include:
#     - remote: 'https://cratly.io/ci/v1/build.yml'
#     - remote: 'https://cratly.io/ci/v1/deploy-s3.yml'
#       inputs: { bucket: example.com, prefix: www }
#
# The job lands in the predefined `build` stage, so a site that declares no
# `stages:` of its own works as-is. One that does must list `build` and the
# stage its deploy job uses.

spec:
  inputs:
    job_name:
      default: "build"
      description: "Name of the generated job."
    stage:
      default: "build"
      description: "Stage to run in. Must exist if the site declares its own `stages:`."
    image:
      default: "oven/bun:latest"
      description: >-
        Container image for the build. The default is the public bun image.
        Point this at a prewarmed image to skip most of the install.
    runtime:
      default: "bun"
      options: [ "bun", "node" ]
      description: "JavaScript runtime used to read the build report. Must exist in `image`."
    install_command:
      default: "bun install --frozen-lockfile"
    build_command:
      default: "bun run build"
    dist_dir:
      default: ".vitepress/dist"
      description: "Where the build writes its output, relative to the project root."
    artifact_dir:
      default: "site"
      description: >-
        Directory the output is moved to before it is stored as an artifact.
        Every cratly deploy template expects the site under this name.
    artifact_expire_in:
      default: "3 days"
    npm_scope:
      default: ""
      description: >-
        Package scope served by a private registry, e.g. "@awesome.me". Leave
        empty for sites that only use public packages. The matching token comes
        from the masked CI/CD variable CRATLY_NPM_TOKEN — never from an input,
        which would be readable by anyone who can read this pipeline.
    npm_registry:
      default: ""
      description: "Registry host serving that scope, without protocol, e.g. npm.example.com."
    fail_on_content_problems:
      default: "true"
      options: [ "true", "false" ]
      description: >-
        Fail merge-request pipelines when the build reports links or images
        pointing at files it does not contain. See the note below.

---

"$[[ inputs.job_name ]]":
  stage: $[[ inputs.stage ]]
  image: $[[ inputs.image ]]
  interruptible: true
  script:
    - |
      set -eu

      # A private scope is configured here rather than baked into the image: a
      # distributable image holding licensed packages would be a licence
      # problem, and GitLab replaces a job image's ENTRYPOINT, so an image
      # cannot configure itself from the environment either.
      if [ -n "$[[ inputs.npm_scope ]]" ]; then
        if [ -z "${CRATLY_NPM_TOKEN:-}" ]; then
          echo "npm_scope is set but CRATLY_NPM_TOKEN is empty — add it as a masked CI/CD variable." >&2
          exit 1
        fi
        {
          printf '%s:registry=https://%s/\n' "$[[ inputs.npm_scope ]]" "$[[ inputs.npm_registry ]]"
          printf '//%s/:_authToken=%s\n' "$[[ inputs.npm_registry ]]" "$CRATLY_NPM_TOKEN"
        } > "$HOME/.npmrc"
      fi
    - $[[ inputs.install_command ]]
    - $[[ inputs.build_command ]]
    # A link or image pointing at a file the build does not contain is a content
    # mistake: the site is finished and works, one link in it does not. The build
    # records it in .cratly/build-report.json rather than failing over it —
    # otherwise a typo would stand between a finished page and its publication,
    # and the reason would sit in this log, which whoever wrote the link cannot
    # read.
    #
    # A merge request is the one place where fixing it is still cheap, so that is
    # where it stops the pipeline. On the default branch the same finding only
    # warns: the deploy has to keep running, or a stale site would be the
    # punishment for a broken link.
    - |
      if [ "$[[ inputs.fail_on_content_problems ]]" = "true" ] \
         && [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ] \
         && [ -f .cratly/build-report.json ]; then
        $[[ inputs.runtime ]] -e '
          const { readFileSync } = require( "fs" );
          const report = JSON.parse( readFileSync( ".cratly/build-report.json", "utf8" ) );
          const problems = report.problems?.content ?? [];
          if ( problems.length ) {
            console.error( `ERROR: ${problems.length} link(s) or image(s) point at files this build does not contain:` );
            for ( const { source, page, reference } of problems ) {
              console.error( `  ${source ?? page} → ${reference}` );
            }
            process.exit( 1 );
          }
        '
      fi
    - rm -rf "$[[ inputs.artifact_dir ]]"
    - mv "$[[ inputs.dist_dir ]]" "$[[ inputs.artifact_dir ]]"
  artifacts:
    name: "site-${CI_COMMIT_REF_SLUG}"
    # Also on failure: the report is the reason the job stopped, and it is what
    # the cratly editor reads to show that reason to whoever wrote the page.
    when: always
    paths:
      - $[[ inputs.artifact_dir ]]/
      - .cratly/build-report.json
    # Short expiry keeps storage lean; GitLab always retains the latest
    # artifact for a ref regardless of this setting.
    expire_in: $[[ inputs.artifact_expire_in ]]
