From eebff2e673ed1c7d778a8659fdbe66f629118292 Mon Sep 17 00:00:00 2001 From: "blackoutsecure-gatewall-aut-c172c5[bot]" <323463760+blackoutsecure-gatewall-aut-c172c5[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 07:34:26 +0000 Subject: [PATCH] chore: install bos-universal-gatekeeper-kicker.yml from the automation hub --- .../bos-universal-gatekeeper-kicker.yml | 796 ++++++++++++++++++ 1 file changed, 796 insertions(+) create mode 100644 .github/workflows/bos-universal-gatekeeper-kicker.yml diff --git a/.github/workflows/bos-universal-gatekeeper-kicker.yml b/.github/workflows/bos-universal-gatekeeper-kicker.yml new file mode 100644 index 0000000..cfe1b84 --- /dev/null +++ b/.github/workflows/bos-universal-gatekeeper-kicker.yml @@ -0,0 +1,796 @@ +# Blackout Secure universal gatekeeper — central kicker (hub-managed). +# +# Single front door for manually dispatched automation in this repository. +# Automatic triggers (schedule/push) keep driving the release pipeline exactly +# as before; `workflow_dispatch` additionally routes to the security, sync, +# action-test, metadata, and Marketplace backends after an authorization gate. +# +# Workflows reachable from this kicker: +# bos-universal-gatekeeper.yml release / deploy / security / metadata stages +# bos-universal-sync.yml managed-file reconciliation +# bos-universal-action-test.yml action smoke tests +# repo-metadata-sync.yml About-box metadata +# bos-universal-marketplace.yml Marketplace validation +# release-promote.yml dev -> main promotion + tagging +# +# Customize via `.github/bos-universal-config.json`, not this file. +# Schema docs: https://github.com/blackoutsecure/bos-automation-hub +# Required vars (names overridable via `.github/bos-universal-config.json`): +# DOCKERHUB_NAMESPACE, BALENA_NAMESPACE +# Required secrets: +# DOCKERHUB_USERNAME, DOCKERHUB_TOKEN, BALENA_API_TOKEN +# UPSTREAM_TOKEN (optional — only for private upstream repos) +# GATEKEEPER_AUTHZ_PAT (required for manual dispatch; read:org [+ +# admin:enterprise when an enterprise-owner check is configured]) +name: Blackout Secure Universal Gatekeeper +run-name: Gatekeeper / ${{ inputs.operation || 'full' }} / ref:${{ github.ref_name }} + +"on": + schedule: + # Upstream-change check: only releases when the monitor stage detects a + # new version (or the `41 3 * * 0` cron below forces one regardless). + - cron: "17 */6 * * *" + # Weekly forced rebuild (Sun 03:41 UTC): re-runs the full pipeline even + # with no upstream version change, so base-image/dependency patches still + # land periodically. Mapped to `force: true` by the "Resolve dispatch" + # step below via `github.event.schedule`. Add further cron entries there + # if more scheduled operations are needed later. + - cron: "41 3 * * 0" + push: + branches: [main] + paths: + - "Dockerfile" + - ".dockerignore" + - "root/**" + - "build/**" + - "scripts/**" + - ".github/bos-universal-config.json" + - ".github/workflows/bos-universal-gatekeeper-kicker.yml" + workflow_dispatch: + inputs: + operation: + description: "Which managed pipeline to run." + type: choice + options: + - full + - release_only + - security_only + - sync_only + - action_test + - metadata + - marketplace_validate + - marketplace_release + default: full + force_run: + description: "Force: run pipeline even if upstream unchanged." + type: boolean + default: false + dry_run: + description: "Preview only — applies to metadata and marketplace_release." + type: boolean + default: false + tag_name: + description: "Optional SemVer tag for marketplace_release; empty auto-bumps." + type: string + default: "" + +permissions: + contents: read + +concurrency: + group: bos-gatekeeper-kicker-${{ github.repository }}-${{ github.ref }}-${{ inputs.operation || 'full' }} + cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' || (inputs.operation != 'release_only' && inputs.operation != 'marketplace_release') }} + +jobs: + # Manual-dispatch access control. Runs FIRST and with no write permission + # so an unauthorized dispatch is rejected before `sync-check` (which holds + # `contents: write`) can commit anything. Policy comes from repo/org vars + # rather than the parsed config so this gate never depends on a job that + # runs after it. Non-dispatch events pass through untouched. + authorize: + name: Authorize dispatch + runs-on: ${{ fromJSON(startsWith(vars.DEFAULT_RUNNER || 'ubuntu-latest', '[') && (vars.DEFAULT_RUNNER || 'ubuntu-latest') || format('"{0}"', vars.DEFAULT_RUNNER || 'ubuntu-latest')) }} + timeout-minutes: 3 + permissions: + contents: read + outputs: + gate_level: ${{ steps.gate.outputs.level }} + steps: + # Opt-in egress auditing. Off by default because `vars.DEFAULT_RUNNER` + # may point at self-hosted capacity where this agent is inappropriate. + - name: Harden runner + if: vars.GATEKEEPER_HARDEN_RUNNER == 'true' + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 + with: + egress-policy: ${{ vars.GATEKEEPER_EGRESS_POLICY || 'audit' }} + + # Gate level scales the checks to the blast radius of the operation: + # `low` only reads, `standard` writes repo state, `high` publishes + # artifacts or moves the stable branch. + - name: Resolve gate level + id: gate + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + OPERATION: ${{ inputs.operation }} + run: | + set -euo pipefail + if [ "${EVENT_NAME}" != "workflow_dispatch" ]; then + echo "level=automatic" >> "${GITHUB_OUTPUT}" + exit 0 + fi + case "${OPERATION:-full}" in + full|release_only|marketplace_release) level=high ;; + sync_only|metadata) level=standard ;; + security_only|marketplace_validate|action_test) level=low ;; + *) level=high ;; + esac + echo "level=${level}" >> "${GITHUB_OUTPUT}" + echo "::notice title=Gate level::${OPERATION:-full} resolved to gate level '${level}'." + + # Short-lived installation token for the org/team lookups. Preferred + # over a PAT: it expires in an hour and is scoped to `members: read`. + - name: Mint authorization token + id: authz_token + if: github.event_name == 'workflow_dispatch' && vars.GATEKEEPER_APP_ID != '' + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ vars.GATEKEEPER_APP_ID }} + private-key: ${{ secrets.GATEKEEPER_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - name: Check dispatcher authorization + uses: blackoutsecure/bos-workflow-gatekeeper@eae1fdd8ab731e72bf890fdb7c1504415d12d88c # v1.0.2 + with: + actor: ${{ github.triggering_actor || github.actor }} + organization: ${{ github.repository_owner }} + repository: ${{ github.repository }} + event_name: ${{ github.event_name }} + enterprise_slug: ${{ vars.GATEKEEPER_ENTERPRISE_SLUG }} + required_teams: ${{ vars.GATEKEEPER_REQUIRED_TEAMS }} + allow_org_admin: ${{ vars.GATEKEEPER_ALLOW_ORG_ADMIN != 'false' }} + # Enterprise ownership is only demanded for `high` gate operations, + # and only when a slug is actually configured — otherwise the check + # is unverifiable and would deny every dispatch. + require_enterprise_owner: ${{ steps.gate.outputs.level == 'high' && vars.GATEKEEPER_ENTERPRISE_SLUG != '' && vars.GATEKEEPER_REQUIRE_ENTERPRISE_OWNER != 'false' }} + fail_closed: true + token: ${{ steps.authz_token.outputs.token || secrets.GATEKEEPER_AUTHZ_PAT }} + enterprise_token: ${{ secrets.GATEKEEPER_AUTHZ_PAT }} + summary: true + + # Cheap, config-free hub-ref resolution so `sync-check` (which must run + # BEFORE `parse-config`, since it's a pre-flight) can still pick the + # matching @dev/@main reusable workflow, same as every dual-branch kicker. + # Almost always resolves to `main` here in practice: Launchpad's `push` + # trigger is `branches: [main]` only, so only `workflow_dispatch` runs + # manually launched from a `dev` branch (or `schedule` on a repo whose + # default branch happens to be `dev`) ever see `ref_name == 'dev'`. + resolve-target-ref: + name: Resolve target hub ref + needs: authorize + runs-on: ${{ fromJSON(startsWith(vars.DEFAULT_RUNNER || 'ubuntu-latest', '[') && (vars.DEFAULT_RUNNER || 'ubuntu-latest') || format('"{0}"', vars.DEFAULT_RUNNER || 'ubuntu-latest')) }} + timeout-minutes: 2 + outputs: + target_ref: ${{ steps.target_ref.outputs.ref }} + steps: + - name: Checkout hub resolver + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: blackoutsecure/bos-automation-hub + ref: ${{ github.ref_name == 'dev' && 'dev' || 'main' }} + path: hub-runtime + sparse-checkout: .github/actions/resolve-hub-ref + sparse-checkout-cone-mode: false + - name: Resolve target hub ref + id: target_ref + uses: ./hub-runtime/.github/actions/resolve-hub-ref + with: + event_name: ${{ github.event_name }} + ref_name: ${{ github.ref_name }} + + # Sync managed files ahead of the release so a stale kicker/config never + # runs against outdated policy. If the sync commits a change to this + # kicker file or the consumer config, defer entirely: the commit's push + # re-triggers a fresh run (see the `push.paths` above), so this run skips + # the rest of the pipeline instead of executing under a spec that's about + # to be superseded. Unrelated managed-file drift (docs, community-health, + # etc.) is committed but does not block this run. + # Skipped on `schedule`: the standalone sync kicker already owns periodic + # reconciliation (weekly cron + immediate config-push trigger) across every + # repo, so the 6h Launchpad cron doesn't need to duplicate that cadence — + # this pre-flight only needs to run on the triggers that can actually carry + # a fresh kicker/config commit into the same run (push, manual dispatch). + # Split dev/main like every other dual-branch kicker (`resolve-target-ref` + # above picks exactly one of the two). + sync-check-dev: + name: Sync managed files (pre-flight, dev) + needs: resolve-target-ref + if: github.event_name != 'schedule' && needs.resolve-target-ref.outputs.target_ref == 'dev' + permissions: + contents: write + uses: blackoutsecure/bos-automation-hub/.github/workflows/bos-universal-sync.yml@dev + with: + mode: commit + secrets: inherit + + sync-check-main: + name: Sync managed files (pre-flight, main) + needs: resolve-target-ref + if: github.event_name != 'schedule' && needs.resolve-target-ref.outputs.target_ref == 'main' + permissions: + contents: write + uses: blackoutsecure/bos-automation-hub/.github/workflows/bos-universal-sync.yml@main + with: + mode: commit + secrets: inherit + + parse-config: + name: Parse universal config + needs: [resolve-target-ref, sync-check-dev, sync-check-main] + # `sync-check-dev`/`sync-check-main` are mutually exclusive (only one + # runs; the other reports `skipped`, not `success`) — tolerate either + # so `always()` doesn't need to fan out across both outcomes below. + if: >- + always() + && !cancelled() + && needs.resolve-target-ref.result == 'success' + && (needs.sync-check-dev.result == 'success' || needs.sync-check-dev.result == 'skipped') + && (needs.sync-check-main.result == 'success' || needs.sync-check-main.result == 'skipped') + && !contains(needs.sync-check-dev.outputs.changed_files, '.github/workflows/bos-universal-gatekeeper-kicker.yml') + && !contains(needs.sync-check-main.outputs.changed_files, '.github/workflows/bos-universal-gatekeeper-kicker.yml') + && !contains(needs.sync-check-dev.outputs.changed_files, '.github/bos-universal-config.json') + && !contains(needs.sync-check-main.outputs.changed_files, '.github/bos-universal-config.json') + # Resolve runner from org-shared `vars.DEFAULT_RUNNER` when set, + # supporting both bare label and JSON-array formats. Fallback to + # `ubuntu-latest` if the variable is not defined in the consumer. + runs-on: ${{ fromJSON(startsWith(vars.DEFAULT_RUNNER || 'ubuntu-latest', '[') && (vars.DEFAULT_RUNNER || 'ubuntu-latest') || format('"{0}"', vars.DEFAULT_RUNNER || 'ubuntu-latest')) }} + timeout-minutes: 2 + outputs: + cfg: ${{ steps.config.outputs.cfg }} + target_ref: ${{ needs.resolve-target-ref.outputs.target_ref }} + operation: ${{ steps.dispatch.outputs.operation }} + force_run: ${{ steps.dispatch.outputs.force }} + run_publish: ${{ steps.dispatch.outputs.run_publish }} + run_security: ${{ steps.dispatch.outputs.run_security }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Load universal config + id: config + uses: blackoutsecure/bos-automation-hub/.github/actions/universal-config@main + with: + config_path: .github/bos-universal-config.json + # Maps each trigger (and, for `schedule`, each individual cron via + # `github.event.schedule`) to one `operation`/`force` pair, so the + # release job below reads one resolved pair instead of repeating the + # per-trigger ternary at every `docker_force`/`balena_force`/etc. input. + - name: Resolve dispatch operation + id: dispatch + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + SCHEDULE: ${{ github.event.schedule }} + DISPATCH_OPERATION: ${{ inputs.operation }} + DISPATCH_FORCE: ${{ inputs.force_run }} + PUSH_FORCE: ${{ fromJson(steps.config.outputs.cfg).triggers.force_on_push }} + run: | + set -euo pipefail + operation=full + force=false + case "${EVENT_NAME}" in + workflow_dispatch) + operation="${DISPATCH_OPERATION:-full}" + [ "${DISPATCH_FORCE}" = "true" ] && force=true + ;; + push) + [ "${PUSH_FORCE}" = "true" ] && force=true + ;; + schedule) + # Add further cron -> operation/force mappings here as needed. + case "${SCHEDULE}" in + '41 3 * * 0') force=true ;; + esac + ;; + esac + echo "operation=${operation}" >> "${GITHUB_OUTPUT}" + echo "force=${force}" >> "${GITHUB_OUTPUT}" + + # Collapse the operation into the two stage families the release + # backend actually gates on, so both the dev and main callers read + # one resolved flag instead of repeating the operation ternary. + run_publish=false + run_security=false + case "${operation}" in + full) run_publish=true; run_security=true ;; + release_only) run_publish=true ;; + security_only) run_security=true ;; + esac + echo "run_publish=${run_publish}" >> "${GITHUB_OUTPUT}" + echo "run_security=${run_security}" >> "${GITHUB_OUTPUT}" + + - name: Validate routing outputs + shell: bash + env: + OPERATION: ${{ steps.dispatch.outputs.operation }} + TARGET_REF: ${{ needs.resolve-target-ref.outputs.target_ref }} + RUN_PUBLISH: ${{ steps.dispatch.outputs.run_publish }} + RUN_SECURITY: ${{ steps.dispatch.outputs.run_security }} + run: | + set -euo pipefail + case "${TARGET_REF}" in dev|main) ;; *) echo "::error::Invalid target_ref '${TARGET_REF}'."; exit 1 ;; esac + case "${OPERATION}" in full|release_only|security_only|sync_only|action_test|metadata|marketplace_validate|marketplace_release) ;; *) echo "::error::Invalid operation '${OPERATION}'."; exit 1 ;; esac + case "${RUN_PUBLISH}:${RUN_SECURITY}" in true:true|true:false|false:true|false:false) ;; *) echo "::error::Invalid stage routing flags."; exit 1 ;; esac + echo "::notice title=Dispatch route::operation=${OPERATION}, target_ref=${TARGET_REF}, publish=${RUN_PUBLISH}, security=${RUN_SECURITY}" + + # Verify the runner actually has the toolchain this repo declares before any + # routed operation starts. Skipped when `gatekeeper.preflight` is absent. + preflight: + name: Runner preflight + needs: [authorize, parse-config] + if: fromJson(needs.parse-config.outputs.cfg).gatekeeper.preflight != '' + runs-on: ${{ fromJSON(startsWith(vars.DEFAULT_RUNNER || 'ubuntu-latest', '[') && (vars.DEFAULT_RUNNER || 'ubuntu-latest') || format('"{0}"', vars.DEFAULT_RUNNER || 'ubuntu-latest')) }} + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Verify declared dependencies + uses: blackoutsecure/bos-workflow-gatekeeper@eae1fdd8ab731e72bf890fdb7c1504415d12d88c # v1.0.2 + with: + preflight_only: true + preflight_spec: ${{ toJson(fromJson(needs.parse-config.outputs.cfg).gatekeeper.preflight) }} + preflight_gate_level: ${{ needs.authorize.outputs.gate_level }} + + # --------------------------------------------------------------------------- + # Dispatch routing. Each job below owns exactly one manual operation and is + # inert on automatic (push/schedule) events, so adding a route here never + # changes the unattended release cadence. Every route inherits the + # `authorize` gate transitively through `parse-config`. + # --------------------------------------------------------------------------- + + # `operation: action_test` — action smoke tests against the repo's own + # `action_test` config block. + action-test: + name: Action test + needs: parse-config + if: github.event_name == 'workflow_dispatch' && inputs.operation == 'action_test' + permissions: + contents: read + models: read + uses: blackoutsecure/bos-automation-hub/.github/workflows/bos-universal-action-test.yml@main + secrets: inherit + + # `operation: metadata` — About-box sync only, without running the release + # pipeline that normally carries the `repo_metadata` stage. + metadata: + name: Repository metadata + needs: parse-config + if: github.event_name == 'workflow_dispatch' && inputs.operation == 'metadata' + permissions: + contents: read + models: read + uses: blackoutsecure/bos-automation-hub/.github/workflows/repo-metadata-sync.yml@main + with: + description: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.description || '' }} + description_mode: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.description_mode || 'auto' }} + homepage: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.homepage || '' }} + topics: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.topics || '' }} + generate_topics: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.generate_topics == true }} + ai_enabled: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.ai_enabled != false }} + ai_model: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.ai_model || 'auto' }} + show_releases: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.show_releases != false }} + show_deployments: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.show_deployments == true }} + show_packages: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.show_packages == true }} + dry_run: ${{ inputs.dry_run == true || fromJson(needs.parse-config.outputs.cfg).repo_metadata.dry_run == true }} + secrets: inherit + + # `operation: marketplace_validate` — Marketplace manifest and rule checks. + # The Marketplace kicker still owns the PR/push-triggered runs; this route + # exists so a maintainer can re-validate on demand from one place. + marketplace-validate: + name: Marketplace validation + needs: parse-config + if: github.event_name == 'workflow_dispatch' && inputs.operation == 'marketplace_validate' + permissions: + contents: read + uses: blackoutsecure/bos-automation-hub/.github/workflows/bos-universal-marketplace.yml@main + with: + action_yml_path: ${{ fromJson(needs.parse-config.outputs.cfg).marketplace.action_yml_path || 'action.yml' }} + run_branding_preview: ${{ fromJson(needs.parse-config.outputs.cfg).marketplace.run_branding_preview != false }} + secrets: inherit + + # `operation: marketplace_release` — promote the source branch to the stable + # branch, tag it, and publish the GitHub Release. + marketplace-release: + name: Marketplace release + needs: parse-config + if: github.event_name == 'workflow_dispatch' && inputs.operation == 'marketplace_release' + permissions: + contents: write + models: read + uses: blackoutsecure/bos-automation-hub/.github/workflows/release-promote.yml@main + with: + source_branch: ${{ fromJson(needs.parse-config.outputs.cfg).marketplace.source_branch || 'dev' }} + target_branch: ${{ fromJson(needs.parse-config.outputs.cfg).marketplace.target_branch || 'main' }} + allowlist_paths: ${{ fromJson(needs.parse-config.outputs.cfg).marketplace.allowlist_paths || 'action.yml' }} + tag_name: ${{ inputs.tag_name || '' }} + dry_run: ${{ inputs.dry_run == true }} + secrets: inherit + + # `sync_only` stops here — `sync-check-{dev,main}` above already ran the + # sync. `action_test`, `metadata`, and the Marketplace operations are + # routed by their own jobs further down rather than through this stage. + release-dev: + name: Release (dev) + needs: parse-config + if: >- + github.ref_name == 'dev' + && (github.event_name != 'workflow_dispatch' || contains(fromJson('["full","release_only","security_only"]'), inputs.operation)) + permissions: + contents: write # monitor tracking-file commit + GitHub Release publish + actions: write # nested monitor (`gh workflow run`) + pull-requests: write # nested Docker Scout PR annotations + security-events: write # nested Docker Scout SARIF upload + models: read # nested release.yml -> github-release.yml AI changelog + uses: blackoutsecure/bos-automation-hub/.github/workflows/bos-universal-gatekeeper.yml@dev + with: + upstream_repo: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.repo || '' }} + source: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.source || 'github_release' }} + upstream_branch: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.branch || '' }} + version_file_path: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.version_file_path || 'version' }} + version_regex: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.version_regex || '' }} + image_ref: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.image_ref || '' }} + package_name: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.package_name || '' }} + version_url: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.version_url || '' }} + tag_pattern: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.tag_pattern || '' }} + track_file: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.track_file || '.github/upstream/tracked-release.json' }} + force_run: ${{ needs.parse-config.outputs.force_run == 'true' }} + + # `run_publish` is resolved once in parse-config from the dispatch + # operation: false for `security_only`, true for `full`/`release_only`. + docker: ${{ needs.parse-config.outputs.run_publish == 'true' && fromJson(needs.parse-config.outputs.cfg).stages.docker == true }} + balena: ${{ needs.parse-config.outputs.run_publish == 'true' && fromJson(needs.parse-config.outputs.cfg).stages.balena == true }} + github_release: ${{ needs.parse-config.outputs.run_publish == 'true' && fromJson(needs.parse-config.outputs.cfg).stages.github_release == true }} + companion_docker: ${{ needs.parse-config.outputs.run_publish == 'true' && fromJson(needs.parse-config.outputs.cfg).stages.companion_docker == true }} + + image_name: ${{ fromJson(needs.parse-config.outputs.cfg).docker.image_name || '' }} + dockerhub_namespace: ${{ vars[fromJson(needs.parse-config.outputs.cfg).docker.namespace_var || 'DOCKERHUB_NAMESPACE'] }} + docker_extra_tags: ${{ fromJson(needs.parse-config.outputs.cfg).docker.extra_tags || '' }} + docker_short_description: ${{ fromJson(needs.parse-config.outputs.cfg).docker.short_description || '' }} + docker_latest: ${{ fromJson(needs.parse-config.outputs.cfg).docker.latest != false }} + docker_multi_arch: ${{ fromJson(needs.parse-config.outputs.cfg).docker.multi_arch != false }} + docker_update_description: ${{ fromJson(needs.parse-config.outputs.cfg).docker.update_description != false }} + docker_force: ${{ needs.parse-config.outputs.force_run == 'true' }} + + # ----- Docker Scout ----- + docker_enable_scout: ${{ fromJson(needs.parse-config.outputs.cfg).scout.enable != false }} + docker_scout_command: ${{ fromJson(needs.parse-config.outputs.cfg).scout.command || 'cves' }} + docker_scout_severities: ${{ fromJson(needs.parse-config.outputs.cfg).scout.severities || 'critical,high' }} + docker_scout_only_fixed: ${{ fromJson(needs.parse-config.outputs.cfg).scout.only_fixed == true }} + docker_scout_ignore_base: ${{ fromJson(needs.parse-config.outputs.cfg).scout.ignore_base == true }} + docker_scout_organization: ${{ fromJson(needs.parse-config.outputs.cfg).scout.organization || '' }} + docker_scout_record_environment: ${{ fromJson(needs.parse-config.outputs.cfg).scout.record_environment || '' }} + docker_scout_sarif_upload: ${{ fromJson(needs.parse-config.outputs.cfg).scout.sarif_upload != false }} + docker_scout_exit_code: ${{ fromJson(needs.parse-config.outputs.cfg).scout.exit_code == true }} + docker_scout_enable_repo: ${{ fromJson(needs.parse-config.outputs.cfg).scout.enable_repo != false }} + + # ----- Balena stage ----- + block_name: ${{ fromJson(needs.parse-config.outputs.cfg).balena.block_name || '' }} + balena_namespace: ${{ vars[fromJson(needs.parse-config.outputs.cfg).balena.namespace_var || 'BALENA_NAMESPACE'] }} + balena_sync_yml: ${{ fromJson(needs.parse-config.outputs.cfg).balena.sync_yml != false }} + balena_draft: ${{ fromJson(needs.parse-config.outputs.cfg).balena.draft == true }} + balena_force: ${{ needs.parse-config.outputs.force_run == 'true' }} + balena_generate_yml: ${{ fromJson(needs.parse-config.outputs.cfg).balena.generate_yml == true }} + balena_type: ${{ fromJson(needs.parse-config.outputs.cfg).balena.type || 'sw.block' }} + balena_repository_url: ${{ fromJson(needs.parse-config.outputs.cfg).balena.repository_url || '' }} + balena_logo_url: ${{ fromJson(needs.parse-config.outputs.cfg).balena.logo_url || '' }} + balena_default_device_type: ${{ fromJson(needs.parse-config.outputs.cfg).balena.default_device_type || '' }} + # `|-` chomps the trailing newline that a plain `|` block would + # add at each forwarding hop, so multi-line caller values stay + # byte-stable through the kicker → launchpad → release.yml chain. + balena_description: |- + ${{ fromJson(needs.parse-config.outputs.cfg).balena.description || '' }} + balena_post_provisioning: |- + ${{ fromJson(needs.parse-config.outputs.cfg).balena.post_provisioning || '' }} + balena_supported_device_types: |- + ${{ fromJson(needs.parse-config.outputs.cfg).balena.supported_device_types || '' }} + + # ----- Companion Docker stage ----- + companion_image_name: ${{ fromJson(needs.parse-config.outputs.cfg).companion_docker.image_name || '' }} + companion_build_target: ${{ fromJson(needs.parse-config.outputs.cfg).companion_docker.build_target || '' }} + companion_docker_short_description: ${{ fromJson(needs.parse-config.outputs.cfg).companion_docker.short_description || '' }} + + # ----- Cloudflare Pages stage ----- + cloudflare_pages: ${{ needs.parse-config.outputs.operation != 'security_only' && fromJson(needs.parse-config.outputs.cfg).stages.cloudflare_pages == true }} + cloudflare_project_name: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.project_name || '' }} + cloudflare_deployment_environment: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.deployment_environment || '' }} + cloudflare_site_url: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.site_url || '' }} + cloudflare_public_dir: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.public_dir || '.' }} + cloudflare_deploy_dir: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.deploy_dir || './dist' }} + cloudflare_clean_deploy_dir: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.clean_deploy_dir != false }} + cloudflare_copy_files: |- + ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.copy_files || '' }} + cloudflare_copy_dirs: |- + ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.copy_dirs || '' }} + cloudflare_prebuild_command: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.prebuild_command || '' }} + cloudflare_working_directory: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.working_directory || '' }} + cloudflare_branch: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.branch || '' }} + cloudflare_commit_message: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.commit_message || '' }} + cloudflare_wrangler_version: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.wrangler_version || '' }} + cloudflare_extra_wrangler_args: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.extra_wrangler_args || '' }} + cloudflare_ai_bindings: ${{ toJson(fromJson(needs.parse-config.outputs.cfg).cloudflare.ai_bindings || fromJson('[]')) }} + cloudflare_deploy_artifact_name: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.deploy_artifact_name || '' }} + cloudflare_deploy: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.deploy || '' }} + cloudflare_runs_on: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.runs_on || '' }} + cloudflare_checkout_fetch_depth: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.checkout_fetch_depth || 0 }} + cloudflare_purge_cache: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.purge_cache != false }} + + # ----- Cloudflare generators ----- + cloudflare_generate_sitemap: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_sitemap == true }} + cloudflare_generate_robots: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_robots == true }} + cloudflare_generate_security_txt: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_security_txt == true }} + cloudflare_generate_humans_txt: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_humans_txt == true }} + cloudflare_security_contact: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.security_contact || '' }} + cloudflare_generator_audit: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generator_audit == true }} + cloudflare_generator_audit_fail_on: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generator_audit_fail_on || 'never' }} + cloudflare_generator_audit_artifact_name: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generator_audit_artifact_name || 'site-compliance-reports' }} + cloudflare_generate_manifest: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_manifest == true }} + cloudflare_manifest_name: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_name || '' }} + cloudflare_manifest_short_name: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_short_name || '' }} + cloudflare_manifest_description: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_description || '' }} + cloudflare_manifest_orientation: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_orientation || '' }} + cloudflare_manifest_theme_color: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_theme_color || '' }} + cloudflare_manifest_background_color: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_background_color || '' }} + cloudflare_manifest_lang: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_lang || '' }} + cloudflare_manifest_dir: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_dir || '' }} + cloudflare_manifest_categories: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_categories || '' }} + cloudflare_manifest_icons_dir: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_icons_dir || '' }} + cloudflare_generate_redirects: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_redirects == true }} + cloudflare_redirects_custom_rules: |- + ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.redirects_custom_rules || '' }} + cloudflare_redirects_replace_existing: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.redirects_replace_existing == true }} + # ----- Cloudflare headers generator ----- + cloudflare_generate_headers: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_headers == true }} + cloudflare_headers_presets: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_presets || 'security,cache' }} + cloudflare_headers_csp: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_csp || '' }} + cloudflare_headers_hsts_max_age: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_hsts_max_age || '63072000' }} + cloudflare_headers_permissions_policy: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_permissions_policy || 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()' }} + cloudflare_headers_cache_html_control: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_cache_html_control || 'public, max-age=0, must-revalidate' }} + cloudflare_headers_cache_assets_pattern: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_cache_assets_pattern || '/assets/*' }} + cloudflare_headers_cache_assets_control: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_cache_assets_control || 'public, max-age=31536000, immutable' }} + cloudflare_generate_common_content_headers: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_common_content_headers != false }} + cloudflare_common_security_txt_cache_control: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.common_security_txt_cache_control || 'public, max-age=86400' }} + cloudflare_common_robots_txt_cache_control: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.common_robots_txt_cache_control || 'public, max-age=86400' }} + cloudflare_common_sitemap_xml_cache_control: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.common_sitemap_xml_cache_control || 'public, max-age=86400' }} + cloudflare_common_humans_txt_cache_control: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.common_humans_txt_cache_control || 'public, max-age=86400' }} + cloudflare_headers_custom_rules: |- + ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_custom_rules || '' }} + cloudflare_headers_replace_existing: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_replace_existing == true }} + # ----- GitHub Release stage ----- + release_template_path: ${{ fromJson(needs.parse-config.outputs.cfg).release.template_path || '' }} + release_extra_context: ${{ fromJson(needs.parse-config.outputs.cfg).release.extra_context || '' }} + generate_release_notes: ${{ fromJson(needs.parse-config.outputs.cfg).release.generate_notes != false }} + release_files: ${{ fromJson(needs.parse-config.outputs.cfg).release.files || '' }} + release_draft: ${{ fromJson(needs.parse-config.outputs.cfg).release.draft == true }} + + # ----- Shared ----- + platforms: ${{ fromJson(needs.parse-config.outputs.cfg).platforms || 'linux/amd64,linux/arm64' }} + + # ----- Universal kicker wiring ----- + use_launchpad_config: true + + # ----- Optional stages configured from data file ----- + enable_security_scan: ${{ needs.parse-config.outputs.operation == 'security_only' || fromJson(needs.parse-config.outputs.cfg).security_scan.enable != false }} + security_scan_fail_on: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.fail_on || 'fail' }} + security_scan_blocks_release: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.blocks_release != false }} + security_scan_enable_kit_composite: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.enable_kit_composite != false }} + security_scan_enable_posture: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.enable_posture != false }} + security_scan_enable_scanners: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.enable_scanners != false }} + security_scan_enable_upload: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.enable_upload != false }} + security_scan_codeql_languages: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.codeql_languages || '' }} + security_scan_codeql_queries: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.codeql_queries || 'security-and-quality' }} + security_scan_codeql_runs_on: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.codeql_runs_on || '' }} + security_scan_use_advanced_pat: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.use_advanced_pat == true }} + + enable_repo_metadata: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.enable == true }} + repo_metadata_description: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.description || '' }} + repo_metadata_description_mode: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.description_mode || 'auto' }} + repo_metadata_description_fallback: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.description_fallback || '' }} + repo_metadata_homepage: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.homepage || '' }} + repo_metadata_topics: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.topics || '' }} + repo_metadata_generate_topics: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.generate_topics == true }} + repo_metadata_topics_fallback: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.topics_fallback || '' }} + repo_metadata_ai_enabled: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.ai_enabled != false }} + repo_metadata_ai_model: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.ai_model || 'auto' }} + repo_metadata_show_releases: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.show_releases != false }} + repo_metadata_show_deployments: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.show_deployments == true }} + repo_metadata_show_packages: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.show_packages == true }} + repo_metadata_dry_run: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.dry_run == true }} + secrets: inherit + + release-main: + name: Release (main) + needs: parse-config + if: >- + github.ref_name == 'main' + && (github.event_name != 'workflow_dispatch' || contains(fromJson('["full","release_only","security_only"]'), inputs.operation)) + permissions: + contents: write # monitor tracking-file commit + GitHub Release publish + actions: write # nested monitor (`gh workflow run`) + pull-requests: write # nested Docker Scout PR annotations + security-events: write # nested Docker Scout SARIF upload + models: read # nested release.yml -> github-release.yml AI changelog + uses: blackoutsecure/bos-automation-hub/.github/workflows/bos-universal-gatekeeper.yml@main + with: + upstream_repo: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.repo || '' }} + source: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.source || 'github_release' }} + upstream_branch: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.branch || '' }} + version_file_path: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.version_file_path || 'version' }} + version_regex: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.version_regex || '' }} + image_ref: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.image_ref || '' }} + package_name: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.package_name || '' }} + version_url: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.version_url || '' }} + tag_pattern: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.tag_pattern || '' }} + track_file: ${{ fromJson(needs.parse-config.outputs.cfg).upstream.track_file || '.github/upstream/tracked-release.json' }} + force_run: ${{ needs.parse-config.outputs.force_run == 'true' }} + + # `run_publish` is resolved once in parse-config from the dispatch + # operation: false for `security_only`, true for `full`/`release_only`. + docker: ${{ needs.parse-config.outputs.run_publish == 'true' && fromJson(needs.parse-config.outputs.cfg).stages.docker == true }} + balena: ${{ needs.parse-config.outputs.run_publish == 'true' && fromJson(needs.parse-config.outputs.cfg).stages.balena == true }} + github_release: ${{ needs.parse-config.outputs.run_publish == 'true' && fromJson(needs.parse-config.outputs.cfg).stages.github_release == true }} + companion_docker: ${{ needs.parse-config.outputs.run_publish == 'true' && fromJson(needs.parse-config.outputs.cfg).stages.companion_docker == true }} + + image_name: ${{ fromJson(needs.parse-config.outputs.cfg).docker.image_name || '' }} + dockerhub_namespace: ${{ vars[fromJson(needs.parse-config.outputs.cfg).docker.namespace_var || 'DOCKERHUB_NAMESPACE'] }} + docker_extra_tags: ${{ fromJson(needs.parse-config.outputs.cfg).docker.extra_tags || '' }} + docker_short_description: ${{ fromJson(needs.parse-config.outputs.cfg).docker.short_description || '' }} + docker_latest: ${{ fromJson(needs.parse-config.outputs.cfg).docker.latest != false }} + docker_multi_arch: ${{ fromJson(needs.parse-config.outputs.cfg).docker.multi_arch != false }} + docker_update_description: ${{ fromJson(needs.parse-config.outputs.cfg).docker.update_description != false }} + docker_force: ${{ needs.parse-config.outputs.force_run == 'true' }} + + # ----- Docker Scout ----- + docker_enable_scout: ${{ fromJson(needs.parse-config.outputs.cfg).scout.enable != false }} + docker_scout_command: ${{ fromJson(needs.parse-config.outputs.cfg).scout.command || 'cves' }} + docker_scout_severities: ${{ fromJson(needs.parse-config.outputs.cfg).scout.severities || 'critical,high' }} + docker_scout_only_fixed: ${{ fromJson(needs.parse-config.outputs.cfg).scout.only_fixed == true }} + docker_scout_ignore_base: ${{ fromJson(needs.parse-config.outputs.cfg).scout.ignore_base == true }} + docker_scout_organization: ${{ fromJson(needs.parse-config.outputs.cfg).scout.organization || '' }} + docker_scout_record_environment: ${{ fromJson(needs.parse-config.outputs.cfg).scout.record_environment || '' }} + docker_scout_sarif_upload: ${{ fromJson(needs.parse-config.outputs.cfg).scout.sarif_upload != false }} + docker_scout_exit_code: ${{ fromJson(needs.parse-config.outputs.cfg).scout.exit_code == true }} + docker_scout_enable_repo: ${{ fromJson(needs.parse-config.outputs.cfg).scout.enable_repo != false }} + + # ----- Balena stage ----- + block_name: ${{ fromJson(needs.parse-config.outputs.cfg).balena.block_name || '' }} + balena_namespace: ${{ vars[fromJson(needs.parse-config.outputs.cfg).balena.namespace_var || 'BALENA_NAMESPACE'] }} + balena_sync_yml: ${{ fromJson(needs.parse-config.outputs.cfg).balena.sync_yml != false }} + balena_draft: ${{ fromJson(needs.parse-config.outputs.cfg).balena.draft == true }} + balena_force: ${{ needs.parse-config.outputs.force_run == 'true' }} + balena_generate_yml: ${{ fromJson(needs.parse-config.outputs.cfg).balena.generate_yml == true }} + balena_type: ${{ fromJson(needs.parse-config.outputs.cfg).balena.type || 'sw.block' }} + balena_repository_url: ${{ fromJson(needs.parse-config.outputs.cfg).balena.repository_url || '' }} + balena_logo_url: ${{ fromJson(needs.parse-config.outputs.cfg).balena.logo_url || '' }} + balena_default_device_type: ${{ fromJson(needs.parse-config.outputs.cfg).balena.default_device_type || '' }} + # `|-` chomps the trailing newline that a plain `|` block would + # add at each forwarding hop, so multi-line caller values stay + # byte-stable through the kicker → launchpad → release.yml chain. + balena_description: |- + ${{ fromJson(needs.parse-config.outputs.cfg).balena.description || '' }} + balena_post_provisioning: |- + ${{ fromJson(needs.parse-config.outputs.cfg).balena.post_provisioning || '' }} + balena_supported_device_types: |- + ${{ fromJson(needs.parse-config.outputs.cfg).balena.supported_device_types || '' }} + + # ----- Companion Docker stage ----- + companion_image_name: ${{ fromJson(needs.parse-config.outputs.cfg).companion_docker.image_name || '' }} + companion_build_target: ${{ fromJson(needs.parse-config.outputs.cfg).companion_docker.build_target || '' }} + companion_docker_short_description: ${{ fromJson(needs.parse-config.outputs.cfg).companion_docker.short_description || '' }} + + # ----- Cloudflare Pages stage ----- + cloudflare_pages: ${{ needs.parse-config.outputs.operation != 'security_only' && fromJson(needs.parse-config.outputs.cfg).stages.cloudflare_pages == true }} + cloudflare_project_name: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.project_name || '' }} + cloudflare_deployment_environment: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.deployment_environment || '' }} + cloudflare_site_url: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.site_url || '' }} + cloudflare_public_dir: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.public_dir || '.' }} + cloudflare_deploy_dir: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.deploy_dir || './dist' }} + cloudflare_clean_deploy_dir: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.clean_deploy_dir != false }} + cloudflare_copy_files: |- + ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.copy_files || '' }} + cloudflare_copy_dirs: |- + ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.copy_dirs || '' }} + cloudflare_prebuild_command: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.prebuild_command || '' }} + cloudflare_working_directory: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.working_directory || '' }} + cloudflare_branch: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.branch || '' }} + cloudflare_commit_message: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.commit_message || '' }} + cloudflare_wrangler_version: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.wrangler_version || '' }} + cloudflare_extra_wrangler_args: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.extra_wrangler_args || '' }} + cloudflare_ai_bindings: ${{ toJson(fromJson(needs.parse-config.outputs.cfg).cloudflare.ai_bindings || fromJson('[]')) }} + cloudflare_deploy_artifact_name: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.deploy_artifact_name || '' }} + cloudflare_deploy: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.deploy || '' }} + cloudflare_runs_on: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.runs_on || '' }} + cloudflare_checkout_fetch_depth: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.checkout_fetch_depth || 0 }} + cloudflare_purge_cache: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.purge_cache != false }} + + # ----- Cloudflare generators ----- + cloudflare_generate_sitemap: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_sitemap == true }} + cloudflare_generate_robots: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_robots == true }} + cloudflare_generate_security_txt: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_security_txt == true }} + cloudflare_generate_humans_txt: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_humans_txt == true }} + cloudflare_security_contact: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.security_contact || '' }} + cloudflare_generator_audit: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generator_audit == true }} + cloudflare_generator_audit_fail_on: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generator_audit_fail_on || 'never' }} + cloudflare_generator_audit_artifact_name: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generator_audit_artifact_name || 'site-compliance-reports' }} + cloudflare_generate_manifest: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_manifest == true }} + cloudflare_manifest_name: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_name || '' }} + cloudflare_manifest_short_name: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_short_name || '' }} + cloudflare_manifest_description: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_description || '' }} + cloudflare_manifest_orientation: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_orientation || '' }} + cloudflare_manifest_theme_color: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_theme_color || '' }} + cloudflare_manifest_background_color: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_background_color || '' }} + cloudflare_manifest_lang: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_lang || '' }} + cloudflare_manifest_dir: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_dir || '' }} + cloudflare_manifest_categories: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_categories || '' }} + cloudflare_manifest_icons_dir: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.manifest_icons_dir || '' }} + cloudflare_generate_redirects: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_redirects == true }} + cloudflare_redirects_custom_rules: |- + ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.redirects_custom_rules || '' }} + cloudflare_redirects_replace_existing: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.redirects_replace_existing == true }} + # ----- Cloudflare headers generator ----- + cloudflare_generate_headers: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_headers == true }} + cloudflare_headers_presets: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_presets || 'security,cache' }} + cloudflare_headers_csp: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_csp || '' }} + cloudflare_headers_hsts_max_age: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_hsts_max_age || '63072000' }} + cloudflare_headers_permissions_policy: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_permissions_policy || 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()' }} + cloudflare_headers_cache_html_control: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_cache_html_control || 'public, max-age=0, must-revalidate' }} + cloudflare_headers_cache_assets_pattern: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_cache_assets_pattern || '/assets/*' }} + cloudflare_headers_cache_assets_control: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_cache_assets_control || 'public, max-age=31536000, immutable' }} + cloudflare_generate_common_content_headers: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.generate_common_content_headers != false }} + cloudflare_common_security_txt_cache_control: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.common_security_txt_cache_control || 'public, max-age=86400' }} + cloudflare_common_robots_txt_cache_control: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.common_robots_txt_cache_control || 'public, max-age=86400' }} + cloudflare_common_sitemap_xml_cache_control: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.common_sitemap_xml_cache_control || 'public, max-age=86400' }} + cloudflare_common_humans_txt_cache_control: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.common_humans_txt_cache_control || 'public, max-age=86400' }} + cloudflare_headers_custom_rules: |- + ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_custom_rules || '' }} + cloudflare_headers_replace_existing: ${{ fromJson(needs.parse-config.outputs.cfg).cloudflare.headers_replace_existing == true }} + # ----- GitHub Release stage ----- + release_template_path: ${{ fromJson(needs.parse-config.outputs.cfg).release.template_path || '' }} + release_extra_context: ${{ fromJson(needs.parse-config.outputs.cfg).release.extra_context || '' }} + generate_release_notes: ${{ fromJson(needs.parse-config.outputs.cfg).release.generate_notes != false }} + release_files: ${{ fromJson(needs.parse-config.outputs.cfg).release.files || '' }} + release_draft: ${{ fromJson(needs.parse-config.outputs.cfg).release.draft == true }} + + # ----- Shared ----- + platforms: ${{ fromJson(needs.parse-config.outputs.cfg).platforms || 'linux/amd64,linux/arm64' }} + + # ----- Universal kicker wiring ----- + use_launchpad_config: true + + # ----- Optional stages configured from data file ----- + enable_security_scan: ${{ needs.parse-config.outputs.operation == 'security_only' || fromJson(needs.parse-config.outputs.cfg).security_scan.enable != false }} + security_scan_fail_on: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.fail_on || 'fail' }} + security_scan_blocks_release: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.blocks_release != false }} + security_scan_enable_kit_composite: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.enable_kit_composite != false }} + security_scan_enable_posture: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.enable_posture != false }} + security_scan_enable_scanners: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.enable_scanners != false }} + security_scan_enable_upload: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.enable_upload != false }} + security_scan_codeql_languages: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.codeql_languages || '' }} + security_scan_codeql_queries: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.codeql_queries || 'security-and-quality' }} + security_scan_codeql_runs_on: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.codeql_runs_on || '' }} + security_scan_use_advanced_pat: ${{ fromJson(needs.parse-config.outputs.cfg).security_scan.use_advanced_pat == true }} + + enable_repo_metadata: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.enable == true }} + repo_metadata_description: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.description || '' }} + repo_metadata_homepage: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.homepage || '' }} + repo_metadata_topics: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.topics || '' }} + repo_metadata_generate_topics: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.generate_topics == true }} + repo_metadata_topics_fallback: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.topics_fallback || '' }} + repo_metadata_ai_enabled: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.ai_enabled != false }} + repo_metadata_ai_model: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.ai_model || 'auto' }} + repo_metadata_show_releases: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.show_releases != false }} + repo_metadata_show_deployments: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.show_deployments == true }} + repo_metadata_show_packages: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.show_packages == true }} + repo_metadata_dry_run: ${{ fromJson(needs.parse-config.outputs.cfg).repo_metadata.dry_run == true }} + secrets: inherit