name: Deploy to Orchestrator on: workflow_run: workflows: ["Build and Push to ACR"] types: [completed] branches: [ "dev", "staging", "prod" ] workflow_dispatch: inputs: environment: description: 'Target environment' required: true default: 'dev' type: choice options: - dev - staging - prod env: PO_API_URL: https://api.dev.orchestrator.crucible.kyndemo.live PO_ORG_ID: crucible PO_AUTH_TOKEN: ${{ secrets.PO_AUTH_TOKEN }} # ONE ORCHESTRATOR PROJECT PER APPLICATION. # # This used to be the shared `apps-cluster` project with one environment per app, which # made every app a peer of every other: the Orchestrator tab on any component listed the # entire estate, and an app had exactly one environment named after itself, so there was # nowhere for dev/staging/prod to live. # # That shape existed to avoid a Terraform pull request against config/projects.tf for every # scaffolded app. That constraint turned out not to be real -- `octl create project`, # `octl create runner-rule` and `octl create environment` are all runtime operations, so # the workflow below builds the whole thing on first deploy and needs no repository change. PROJECT_ID: reno-demo-16 # The runner a project's workloads execute on. This is NOT cosmetic: the Kubernetes and # Helm providers are ambient, so a workload lands in whichever cluster its runner lives in, # and Terraform state is keyed per runner. A project bound to the wrong runner deploys to # the wrong cluster, and repointing it afterwards orphans the state it already owns. PO_RUNNER_ID: crucible-orchestrator-dev-apps-dev-runner OCTL_VERSION: 1.0.0 IMAGE: bstagecjotdevacr.azurecr.io/reno-demo-16 jobs: guard: name: Platform guard runs-on: ubuntu-latest outputs: ready: ${{ steps.check.outputs.ready }} steps: - uses: actions/checkout@v4 - name: Check platform initialized id: check run: | if [ -f ".platform/initialized.md" ]; then echo "ready=true" >> $GITHUB_OUTPUT else echo "ready=false" >> $GITHUB_OUTPUT echo "Skipping: .platform/initialized.md not found" fi deploy: name: Deploy to Orchestrator needs: guard if: >- (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && needs.guard.outputs.ready == 'true') || (github.event_name == 'workflow_dispatch') runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Install octl run: | set -euo pipefail curl -fsSLo /tmp/octl.tar.gz \ "https://github.com/stellwerk-labs/platform-orchestrator-cli/releases/download/v${OCTL_VERSION}/platform-orchestrator-cli_${OCTL_VERSION}_linux_amd64.tar.gz" tar xzf /tmp/octl.tar.gz -C /tmp install -m 755 /tmp/octl /usr/local/bin/octl octl --version - name: Derive environment run: | # The environment is now a STAGE of this application -- dev, staging, prod -- because # the project is the application. It used to be the component id, which was the only # option while every app shared one project and had to be distinguishable inside it. DISPATCH_ENV="${{ github.event.inputs.environment }}" if [ -n "$DISPATCH_ENV" ]; then ENV_ID="$DISPATCH_ENV" else # On a workflow_run the branch is the triggering run's, not this job's checkout. BRANCH="${{ github.event.workflow_run.head_branch }}" BRANCH="${BRANCH:-${GITHUB_REF_NAME}}" case "$BRANCH" in staging) ENV_ID=staging ;; prod|main|master) ENV_ID=prod ;; *) ENV_ID=dev ;; esac echo "Branch '$BRANCH' maps to environment '$ENV_ID'" fi echo "ENV_ID=$ENV_ID" >> $GITHUB_ENV echo "Deploying $PROJECT_ID to environment: $ENV_ID" - name: Ensure the project and its runner binding exist run: | set -euo pipefail # Created on first deploy rather than by a pull request against config/projects.tf. # None of this needs a repository change: project, runner rule and environment are # all runtime objects. # # Neither create is idempotent, so both fall through to a read on the second run. octl create project "$PROJECT_ID" \ --set display_name='reno-demo-16' || \ octl get project "$PROJECT_ID" # The runner rule is what routes this project's deployments to the apps cluster. # Creating it twice would leave two rules matching the same project, so it is # created only when absent -- `create` would happily add a duplicate. if octl get runner-rules -o json 2>/dev/null | grep -q "\"project_id\": *\"$PROJECT_ID\""; then echo "Runner rule for '$PROJECT_ID' already exists." else octl create runner-rule \ --set project_id="$PROJECT_ID" \ --set runner_id="$PO_RUNNER_ID" \ --no-prompt fi - name: Ensure the environment exists run: | set -euo pipefail # Project and environment ids are POSITIONAL; only env_type_id and display_name go # through --set. `dev` and `stable` are the only environment TYPES that exist, so # staging rides on the dev type -- the type governs policy, the id governs identity. case "$ENV_ID" in prod) ENV_TYPE=stable ;; *) ENV_TYPE=dev ;; esac octl create environment "$PROJECT_ID" "$ENV_ID" \ --set env_type_id="$ENV_TYPE" \ --set display_name="$ENV_ID" || \ octl get environment "$PROJECT_ID" "$ENV_ID" - name: Deploy the Score workload run: | set -euo pipefail # `octl score deploy` is ADDITIVE — it adds or updates a workload in the manifest and # never removes one. That is the opposite of `octl deploy`, where omission is # deletion. Do not substitute one for the other. # No --show-logs: octl 1.0.0 has no such flag and exits 1 with `unknown flag` # BEFORE contacting the orchestrator, so the whole deploy dies on an argument # typo. Its nearest relatives are --runner-logs-level (default `info`, already # what we want) and --skip-logs (which suppresses storage). Neither streams the # runner's logs into this job, so there is nothing to substitute -- the runner # logs are read from the orchestrator, not from here. # The tag must be the commit the BUILD built, and it must be the WHOLE sha. # # build-push.yml tags with `` -- all 40 characters -- so the # 7-character `${GITHUB_SHA:0:7}` this used to pass named a tag that has never # existed in the registry. # # And on a workflow_run, GITHUB_SHA is the DEFAULT branch's head, while the build # that produced the image ran on `dev`. They coincide only while the branches are # level. `workflow_run.head_sha` is the triggering run's own commit, which is by # definition the one that was built; `github.sha` covers the workflow_dispatch case, # where there is no triggering run. IMAGE_TAG="${{ github.event.workflow_run.head_sha || github.sha }}" echo "Deploying ${IMAGE}:${IMAGE_TAG}" octl score deploy "$PROJECT_ID" "$ENV_ID" score.yaml \ --default-image "${IMAGE}:${IMAGE_TAG}" \ --no-prompt - name: Deployment summary if: always() run: | # Same commit the deploy step resolved, abbreviated for reading only -- the # deployed tag is the full sha. DEPLOYED_SHA="${{ github.event.workflow_run.head_sha || github.sha }}" SHORT_SHA="${DEPLOYED_SHA:0:7}" echo "## Deployment Result" >> $GITHUB_STEP_SUMMARY echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY echo "|---|---|" >> $GITHUB_STEP_SUMMARY echo "| Project | \`$PROJECT_ID\` |" >> $GITHUB_STEP_SUMMARY echo "| Environment | \`$ENV_ID\` |" >> $GITHUB_STEP_SUMMARY echo "| Commit | \`$SHORT_SHA\` |" >> $GITHUB_STEP_SUMMARY echo "[View in Orchestrator Console](https://console.dev.orchestrator.crucible.kyndemo.live/orgs/$PO_ORG_ID/projects/$PROJECT_ID/environments/$ENV_ID)" >> $GITHUB_STEP_SUMMARY