diff --git a/.github/workflows/release-traceability.yml b/.github/workflows/release-traceability.yml index 054b05a..38636d7 100644 --- a/.github/workflows/release-traceability.yml +++ b/.github/workflows/release-traceability.yml @@ -26,6 +26,18 @@ jobs: SERVICE_IMAGE_LATEST_REF: ghcr.io/${{ github.repository }}:latest run: bash ./scripts/github-actions/build-service-image.sh + - name: Build linux binary artifact + env: + BILLING_SERVICE_BINARY_ARTIFACT: dist/billing-service-linux-amd64 + run: bash ./scripts/github-actions/build-service-binary.sh + + - name: Upload billing-service binary artifact + uses: actions/upload-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: billing-service-linux-amd64 + path: dist/billing-service-linux-amd64 + if-no-files-found: error + - name: Push image run: bash ./scripts/github-actions/push-image-placeholder.sh @@ -35,12 +47,32 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Download billing-service binary artifact + uses: actions/download-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: billing-service-linux-amd64 + path: dist + + - name: Install ansible + run: sudo apt-get update && sudo apt-get install -y ansible + + - name: Configure deploy SSH + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + run: | + test -n "${SSH_PRIVATE_KEY}" + install -d -m 0700 ~/.ssh + printf '%s\n' "${SSH_PRIVATE_KEY}" > ~/.ssh/id_ed25519 + chmod 0600 ~/.ssh/id_ed25519 + ssh-keyscan -H jp-xhttp-contabo.svc.plus >> ~/.ssh/known_hosts + - name: Deploy via playbook env: - IMAGE_REF: ${{ needs.build.outputs.service_image_ref }} BILLING_SERVICE_IMAGE_REF: ${{ needs.build.outputs.service_image_ref }} - BILLING_SERVICE_IMAGE_TAG: ${{ needs.build.outputs.service_image_tag }} - BILLING_SERVICE_IMAGE_COMMIT: ${{ needs.build.outputs.service_image_commit }} + BILLING_SERVICE_BINARY_ARTIFACT: dist/billing-service-linux-amd64 + DATABASE_URL: ${{ secrets.DATABASE_URL }} + INTERNAL_SERVICE_TOKEN: ${{ secrets.INTERNAL_SERVICE_TOKEN }} + STACK_TARGET_HOST: jp-xhttp-contabo.svc.plus run: bash ./scripts/github-actions/deploy-billing-service.sh validate: @@ -57,8 +89,18 @@ jobs: - name: Verify traceability script cases run: bash ./scripts/github-actions/test-validate-release-traceability.sh + - name: Configure validate SSH + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + run: | + test -n "${SSH_PRIVATE_KEY}" + install -d -m 0700 ~/.ssh + printf '%s\n' "${SSH_PRIVATE_KEY}" > ~/.ssh/id_ed25519 + chmod 0600 ~/.ssh/id_ed25519 + ssh-keyscan -H jp-xhttp-contabo.svc.plus >> ~/.ssh/known_hosts + - name: Validate runtime traceability env: SERVICE_IMAGE_REF: ${{ needs.build.outputs.service_image_ref }} - RUNTIME_PING_URL: https://accounts.svc.plus/api/ping - run: bash ./scripts/github-actions/validate-release-traceability.sh + STACK_TARGET_HOST: jp-xhttp-contabo.svc.plus + run: bash ./scripts/github-actions/validate-release-traceability-remote.sh diff --git a/docs/release-traceability.md b/docs/release-traceability.md index dc86560..7a714ab 100644 --- a/docs/release-traceability.md +++ b/docs/release-traceability.md @@ -5,7 +5,9 @@ release identity. ## Runtime contract -- `IMAGE` must contain the full image reference used to start the container. +- `IMAGE` must contain the full release image reference produced by the build + job, even when the target host runs the service as a systemd binary instead + of a container. - `/api/ping` returns `image`, `tag`, `commit`, and `version`. - `tag`, `commit`, and `version` are derived from `IMAGE`. - If `IMAGE` is missing or malformed, the derived fields stay empty instead of @@ -14,9 +16,13 @@ release identity. ## Pipeline contract - Build must produce `service_image_ref` only from the full `GITHUB_SHA`. -- Deploy must consume `service_image_ref` and pass it through as the runtime - image identity. -- Validate must use `GET https://accounts.svc.plus/api/ping`. +- Build must also produce the linux billing-service binary artifact consumed by + deploy. +- Deploy must consume that build artifact directly and must not rebuild on the + target host. +- Deploy must pass `service_image_ref` through as the runtime image identity. +- Validate must query `billing-service` on the deployment target at + `http://127.0.0.1:8081/api/ping` over SSH. - Validate must derive `tag` and `commit` from `service_image_ref` and compare them against `/api/ping`. - Validate must fail when runtime `image`, `tag`, `commit`, or `version` is @@ -27,11 +33,11 @@ release identity. ## External playbook alignment The external `playbooks/deploy_billing_service.yml` playbook should accept -`IMAGE_REF` (or an equivalent full image reference variable), derive any -repo/tag helpers from it, and inject `IMAGE=` into the running -container environment for the service exposed through -`https://accounts.svc.plus/api/ping`. +`BILLING_SERVICE_BINARY_ARTIFACT` and `BILLING_SERVICE_IMAGE_REF`, deploy the +binary artifact to the target host without rebuilding it there, and inject +`IMAGE=` into `/etc/default/billing-service` for the runtime +served through `http://127.0.0.1:8081/api/ping`. -If `accounts.svc.plus/api/ping` keeps returning empty runtime metadata, treat +If `billing-service /api/ping` keeps returning empty runtime metadata, treat that as a deployment contract failure: the runtime did not receive the full `IMAGE` value and release traceability is broken. diff --git a/scripts/github-actions/build-service-binary.sh b/scripts/github-actions/build-service-binary.sh new file mode 100644 index 0000000..c90902b --- /dev/null +++ b/scripts/github-actions/build-service-binary.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +artifact_path="${BILLING_SERVICE_BINARY_ARTIFACT:?BILLING_SERVICE_BINARY_ARTIFACT is required}" +target_dir="$(dirname "${artifact_path}")" + +mkdir -p "${target_dir}" + +CGO_ENABLED="${CGO_ENABLED:-0}" \ +GOOS="${GOOS:-linux}" \ +GOARCH="${GOARCH:-amd64}" \ +go build -buildvcs=false -o "${artifact_path}" ./cmd/billing-service + +chmod 0755 "${artifact_path}" diff --git a/scripts/github-actions/deploy-billing-service.sh b/scripts/github-actions/deploy-billing-service.sh index ae5c976..19f9702 100644 --- a/scripts/github-actions/deploy-billing-service.sh +++ b/scripts/github-actions/deploy-billing-service.sh @@ -1,5 +1,36 @@ #!/usr/bin/env bash set -euo pipefail -test -n "${IMAGE_REF:?IMAGE_REF is required}" -ansible-playbook -i inventory playbooks/deploy_billing_service.yml +target_host="${STACK_TARGET_HOST:?STACK_TARGET_HOST is required}" +artifact_path="${BILLING_SERVICE_BINARY_ARTIFACT:?BILLING_SERVICE_BINARY_ARTIFACT is required}" +image_ref="${BILLING_SERVICE_IMAGE_REF:-${IMAGE_REF:-}}" +database_url="${DATABASE_URL:?DATABASE_URL is required}" +internal_service_token="${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}" +playbooks_repo_url="${PLAYBOOKS_REPO_URL:-https://github.com/x-evor/playbooks.git}" +playbooks_repo_ref="${PLAYBOOKS_REPO_REF:-c0f1a1c2ee00e4131db2484c8cc00b2bc4dc1263}" + +if [[ ! -f "${artifact_path}" ]]; then + echo "binary artifact not found: ${artifact_path}" >&2 + exit 1 +fi + +if [[ -z "${image_ref}" ]]; then + echo "BILLING_SERVICE_IMAGE_REF or IMAGE_REF is required" >&2 + exit 1 +fi + +workdir="$(mktemp -d)" +trap 'rm -rf "${workdir}"' EXIT + +git clone --depth 1 "${playbooks_repo_url}" "${workdir}/playbooks" +git -C "${workdir}/playbooks" fetch --depth 1 origin "${playbooks_repo_ref}" +git -C "${workdir}/playbooks" checkout --detach FETCH_HEAD + +export ANSIBLE_HOST_KEY_CHECKING=false +export BILLING_SERVICE_BINARY_ARTIFACT="$(cd "$(dirname "${artifact_path}")" && pwd)/$(basename "${artifact_path}")" +export BILLING_SERVICE_IMAGE_REF="${image_ref}" +export DATABASE_URL="${database_url}" +export INTERNAL_SERVICE_TOKEN="${internal_service_token}" + +cd "${workdir}/playbooks" +ansible-playbook -i inventory.ini deploy_billing_service.yml --limit "${target_host}" diff --git a/scripts/github-actions/test-release-traceability-workflow.sh b/scripts/github-actions/test-release-traceability-workflow.sh index 200c195..5018511 100644 --- a/scripts/github-actions/test-release-traceability-workflow.sh +++ b/scripts/github-actions/test-release-traceability-workflow.sh @@ -30,6 +30,34 @@ if not any(line.strip() == "- build" for line in validate_block): if not any(line.strip() == "- deploy" for line in validate_block): raise SystemExit("validate job must depend on deploy") +build_block = [] +deploy_block = [] +current_job = None + +for line in lines: + if line.startswith(" build:"): + current_job = "build" + elif line.startswith(" deploy:"): + current_job = "deploy" + elif line.startswith(" validate:"): + current_job = "validate" + elif line.startswith(" ") and not line.startswith(" "): + current_job = None + + if current_job == "build": + build_block.append(line) + elif current_job == "deploy": + deploy_block.append(line) + +if not any("Upload billing-service binary artifact" in line for line in build_block): + raise SystemExit("build job must upload the billing-service binary artifact") + +if not any("Download billing-service binary artifact" in line for line in deploy_block): + raise SystemExit("deploy job must download the billing-service binary artifact") + +if not any("BILLING_SERVICE_IMAGE_REF: ${{ needs.build.outputs.service_image_ref }}" in line for line in deploy_block): + raise SystemExit("deploy job must consume needs.build.outputs.service_image_ref") + if not any("SERVICE_IMAGE_REF: ${{ needs.build.outputs.service_image_ref }}" in line for line in validate_block): raise SystemExit("validate job must consume needs.build.outputs.service_image_ref") PY diff --git a/scripts/github-actions/validate-release-traceability-remote.sh b/scripts/github-actions/validate-release-traceability-remote.sh new file mode 100644 index 0000000..c63e7ee --- /dev/null +++ b/scripts/github-actions/validate-release-traceability-remote.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +service_image_ref="${SERVICE_IMAGE_REF:?SERVICE_IMAGE_REF is required}" +target_host="${STACK_TARGET_HOST:?STACK_TARGET_HOST is required}" +ssh_target="${RUNTIME_SSH_TARGET:-root@${target_host}}" +runtime_ping_path="${RUNTIME_PING_PATH:-http://127.0.0.1:8081/api/ping}" +tag="${service_image_ref##*:}" +commit="${tag#sha-}" + +ssh -o BatchMode=yes "${ssh_target}" "systemctl is-active billing-service >/dev/null" + +runtime_payload="$(ssh -o BatchMode=yes "${ssh_target}" "curl -fsS ${runtime_ping_path}")" + +jq -e \ + --arg image "${service_image_ref}" \ + --arg tag "${tag}" \ + --arg commit "${commit}" \ + ' + (.image | type == "string" and length > 0) and + (.tag | type == "string" and length > 0) and + (.commit | type == "string" and length > 0) and + (.version | type == "string" and length > 0) and + .image == $image and + .tag == $tag and + .commit == $commit and + .version == $commit + ' <<<"${runtime_payload}" >/dev/null