From 7905f32c01faeefee8acef10555beb76479151d9 Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sun, 20 Sep 2026 10:28:04 +0530 Subject: [PATCH 1/7] ci: consolidate CI into parallel streamlined jobs Merge go-unit, go-race and go-quality into a single backend-ubuntu job (race, coverage, vet, staticcheck) and introduce distro-tags loop to validate all non-ubuntu build tags plus the untagged distro guard on one host. Merge dashboard unit, quality and coverage into one dashboard job, simplify web and backend build jobs, add repo-validation for OpenAPI, actionlint and shellcheck, and standardize timeouts and env versions. Remove coverage baseline comparison and actionlint config now handled by the repo validation job. --- .github/actionlint.yaml | 5 - .github/coverage-baseline.env | 2 - .github/workflows/ci.yml | 367 +++++++++++++++++----------------- 3 files changed, 181 insertions(+), 193 deletions(-) delete mode 100644 .github/actionlint.yaml delete mode 100644 .github/coverage-baseline.env diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml deleted file mode 100644 index e12b081..0000000 --- a/.github/actionlint.yaml +++ /dev/null @@ -1,5 +0,0 @@ -self-hosted-runner: - labels: - - tako-vm-*-amd64 - - tako-vm-*-arm64 -config-variables: null diff --git a/.github/coverage-baseline.env b/.github/coverage-baseline.env deleted file mode 100644 index 940119b..0000000 --- a/.github/coverage-baseline.env +++ /dev/null @@ -1,2 +0,0 @@ -# Informational until the first stable CI run records the hosted-runner value. -GO_STATEMENT_COVERAGE_BASELINE=42.6 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ec0d06..3bcd2e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: push: + branches: [main] pull_request: workflow_call: @@ -14,15 +15,94 @@ concurrency: env: BUN_VERSION: 1.4.0 + ACTIONLINT_VERSION: 1.7.7 + REDOCLY_CLI_VERSION: 1.34.0 + STATICCHECK_VERSION: 2026.1 +# All jobs run in parallel with no `needs:` edges for fast failure. +# No per-job path filtering: shared files (package.json, bun.lock, nx.json, +# Go workspace/module files, .github/**, tools/**, packaging/**) can affect +# several apps, so every job always runs. Predictable over clever. +# Runner count per normal PR: 7 (was 3). distro-tags loops 7 tags in one +# runner instead of a 7-way matrix; true distro package builds stay in +# native-packages.yml / release.yml, not PR CI. jobs: - go-unit: - name: Go unit tests and coverage + repo-validation: + name: Repo validation runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: ${{ env.BUN_VERSION }} + - name: Lint OpenAPI + run: bunx --bun @redocly/cli@${{ env.REDOCLY_CLI_VERSION }} lint --config .redocly.yaml apps/backend/api/openapi.yaml + - name: Install actionlint + env: + ACTIONLINT_VERSION: ${{ env.ACTIONLINT_VERSION }} + run: | + set -euo pipefail + arch=$(uname -m) + case "$arch" in + x86_64) arch=amd64 ;; + aarch64) arch=arm64 ;; + *) echo "unsupported arch: $arch" >&2; exit 1 ;; + esac + bindir="${RUNNER_TEMP}/bin" + mkdir -p "$bindir" + curl -fsSL -o "$RUNNER_TEMP/actionlint.tar.gz" \ + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_${arch}.tar.gz" + tar -xzf "$RUNNER_TEMP/actionlint.tar.gz" -C "$bindir" actionlint + echo "$bindir" >> "$GITHUB_PATH" + - run: actionlint + - name: Run shellcheck + run: | + set -euo pipefail + # ubuntu-24.04 images already ship shellcheck; never run + # apt-get update just for this. Fall back to install only when + # the preinstalled tool is genuinely absent. + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install --yes --no-install-recommends shellcheck + fi + shellcheck --severity=error \ + tools/package \ + tools/package-check \ + tools/tako-go.sh \ + tools/tako-host-test \ + tools/tako-integration \ + apps/backend/packaging/*.sh \ + apps/backend/packaging/scripts/deb/*.sh \ + apps/backend/packaging/scripts/rpm/*.sh \ + apps/backend/packaging/scripts/arch/*.sh + - name: Check shell syntax + run: | + set -euo pipefail + for script in \ + tools/package \ + tools/package-check \ + tools/tako-go.sh \ + apps/backend/packaging/scripts/test.sh \ + apps/backend/packaging/scripts/deb/*.sh \ + apps/backend/packaging/scripts/rpm/*.sh \ + apps/backend/packaging/scripts/arch/*.sh; do + sh -n "$script" + done + for script in \ + tools/tako-host-test \ + tools/tako-integration \ + apps/backend/packaging/smoke-test.sh \ + apps/backend/packaging/lifecycle-test.sh; do + bash -n "$script" + done + + backend-ubuntu: + name: Backend (ubuntu, authoritative) + runs-on: ubuntu-24.04 + timeout-minutes: 30 env: TAKO_DISTRO: ubuntu - GOCACHE: /tmp/tako-go-ci-cache - GO_COVERAGE_ENFORCE: ${{ vars.GO_COVERAGE_ENFORCE || 'false' }} steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-linux-backend @@ -33,41 +113,17 @@ jobs: cache-dependency-path: | apps/backend/go.sum go.work.sum - - name: Run Go suite with coverage + - name: Run Go race tests with coverage working-directory: apps/backend run: | set -euo pipefail mkdir -p "$GITHUB_WORKSPACE/coverage/go" - ../../tools/tako-go.sh test \ - -covermode=atomic \ - -coverprofile="$GITHUB_WORKSPACE/coverage/go/coverage.out" \ - ./... + ../../tools/tako-go.sh test -race -covermode=atomic \ + -coverprofile="$GITHUB_WORKSPACE/coverage/go/coverage.out" ./... go tool cover -func="$GITHUB_WORKSPACE/coverage/go/coverage.out" \ | tee "$GITHUB_WORKSPACE/coverage/go/coverage.txt" go tool cover -html="$GITHUB_WORKSPACE/coverage/go/coverage.out" \ -o "$GITHUB_WORKSPACE/coverage/go/coverage.html" - - name: Report Go coverage baseline - working-directory: apps/backend - run: | - set -euo pipefail - baseline=$(sed -n 's/^GO_STATEMENT_COVERAGE_BASELINE=//p' "$GITHUB_WORKSPACE/.github/coverage-baseline.env") - current=$(awk '/^total:/ { gsub("%", "", $NF); print $NF }' "$GITHUB_WORKSPACE/coverage/go/coverage.txt") - test -n "$baseline" - test -n "$current" - enforcement="informational until the first stable CI baseline run" - if [ "${GO_COVERAGE_ENFORCE:-false}" = true ]; then - enforcement="blocking" - fi - { - echo "### Go coverage" - echo "- Current statement coverage: ${current}%" - echo "- Recorded baseline: ${baseline}%" - echo "- Enforcement: ${enforcement}" - } >> "$GITHUB_STEP_SUMMARY" - if [ "${GO_COVERAGE_ENFORCE:-false}" = true ] && awk "BEGIN { exit !($current < $baseline) }"; then - echo "Go coverage regressed from ${baseline}% to ${current}%" >&2 - exit 1 - fi - name: Upload Go coverage if: ${{ always() }} uses: actions/upload-artifact@v4 @@ -76,30 +132,24 @@ jobs: path: coverage/go/ if-no-files-found: warn retention-days: 14 - - go-race: - name: Go race tests - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup-linux-backend - - uses: actions/setup-go@v5 - with: - go-version-file: apps/backend/go.mod - cache: true - cache-dependency-path: | - apps/backend/go.sum - go.work.sum - - name: Run Go race suite + - name: Run go vet working-directory: apps/backend + run: ../../tools/tako-go.sh vet ./... + - name: Install staticcheck env: - TAKO_DISTRO: ubuntu - GOCACHE: /tmp/tako-go-race-cache - run: ../../tools/tako-go.sh test -race ./... + GOBIN: ${{ runner.temp }}/bin + run: | + mkdir -p "$GOBIN" + go install honnef.co/go/tools/cmd/staticcheck@${{ env.STATICCHECK_VERSION }} + echo "$GOBIN" >> "$GITHUB_PATH" + - name: Run staticcheck + working-directory: apps/backend + run: staticcheck -checks=all,-U1000,-SA1019,-S1017,-S1016,-ST1000,-ST1020 -tags ubuntu ./... - go-quality: - name: Go vet and static analysis + distro-tags: + name: Distro tags runs-on: ubuntu-24.04 + timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-linux-backend @@ -110,54 +160,54 @@ jobs: cache-dependency-path: | apps/backend/go.sum go.work.sum - - name: Install staticcheck - env: - GOBIN: ${{ runner.temp }}/bin - run: | - mkdir -p "$GOBIN" - go install honnef.co/go/tools/cmd/staticcheck@2026.1 - echo "$GOBIN" >> "$GITHUB_PATH" - - name: Run go vet + - name: Validate every distro build tag on one host working-directory: apps/backend - env: - TAKO_DISTRO: ubuntu - GOCACHE: /tmp/tako-go-quality-cache - run: ../../tools/tako-go.sh vet ./... - - name: Run staticcheck + run: | + set -euo pipefail + # Same Ubuntu host for all tags: validates tag-gated Go code + # compiles and unit tests pass per distro. Not true distro + # environments; real package/install testing stays in + # native-packages.yml. Ubuntu is excluded here: backend-ubuntu + # (race, coverage, vet, staticcheck) and build already cover it. + # No race, coverage, or staticcheck here; backend-ubuntu remains + # the authoritative quality job. `go list` is omitted: vet, test, + # and build each resolve the full package graph for the selected + # tag, so list would prove nothing extra. + # No tag needs runner isolation: each tako-go.sh invocation is a + # fresh `go` process with its own -tags value, no daemon or + # filesystem state leaks between iterations. + for tag in debian fedora rhel rocky almalinux opensuse archlinux; do + echo "--- TAKO_DISTRO=$tag ---" + TAKO_DISTRO="$tag" ../../tools/tako-go.sh vet ./... + TAKO_DISTRO="$tag" ../../tools/tako-go.sh test ./... + TAKO_DISTRO="$tag" ../../tools/tako-go.sh build ./... + done + - name: Verify untagged build fails on the distro guard working-directory: apps/backend - env: - GOCACHE: /tmp/tako-go-quality-cache - STATICCHECK_CACHE: ${{ runner.temp }}/staticcheck-cache - # The first staticcheck baseline keeps pre-existing dead-code and - # package-comment findings informational; all other checks are gated. - run: staticcheck -checks=all,-U1000,-SA1019,-S1017,-S1016,-ST1000,-ST1020 -tags ubuntu ./... - - dashboard-tests: - name: Dashboard unit tests - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: ${{ env.BUN_VERSION }} - - run: bun install --frozen-lockfile - - run: bun nx test dashboard - - dashboard-quality: - name: Dashboard typecheck and lint - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: ${{ env.BUN_VERSION }} - - run: bun install --frozen-lockfile - - run: bun nx typecheck dashboard - - run: bun nx lint dashboard + run: | + set -euo pipefail + # updates_tag_required.go references undefined + # distro_build_tag_required so a build without a supported tag + # fails. Assert the guard reason, not just any failure. + set +e + output=$(go build ./... 2>&1) + build_status=$? + set -e + if [ "$build_status" -eq 0 ]; then + echo "expected untagged backend build to fail" >&2 + exit 1 + fi + if ! printf '%s\n' "$output" | grep -Fq 'distro_build_tag_required'; then + echo "untagged build failed, but not because of the distro-tag guard:" >&2 + printf '%s\n' "$output" >&2 + exit 1 + fi + echo "untagged build correctly rejected: distro_build_tag_required" - dashboard-coverage: - name: Dashboard coverage + dashboard: + name: Dashboard runs-on: ubuntu-24.04 + timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 @@ -173,10 +223,17 @@ jobs: path: apps/dashboard/coverage/ if-no-files-found: warn retention-days: 14 + - run: bun nx typecheck dashboard + - run: bun nx lint dashboard + # No production build here. The dashboard production build runs exactly + # once in the build job via `nx build backend` (dependsOn ^build). + # Separate runners share no local Nx cache, so building here too would + # mean building twice with no benefit. web: - name: Web typecheck, lint, and build + name: Web runs-on: ubuntu-24.04 + timeout-minutes: 20 env: ASTRO_TELEMETRY_DISABLED: "1" steps: @@ -185,12 +242,17 @@ jobs: with: bun-version: ${{ env.BUN_VERSION }} - run: bun install --frozen-lockfile - - run: bun run web:check - - run: bun run web:build + - run: bun nx lint web + # Typecheck runs through the build target's dependsOn graph, not as a + # separate step. + - run: bun nx build web - backend-build: - name: Backend binary build + build: + name: Build runs-on: ubuntu-24.04 + timeout-minutes: 30 + env: + TAKO_DISTRO: ubuntu steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-linux-backend @@ -205,15 +267,22 @@ jobs: with: bun-version: ${{ env.BUN_VERSION }} - run: bun install --frozen-lockfile - - name: Build backend for Ubuntu - env: - TAKO_DISTRO: ubuntu - run: bun nx build backend + # Builds the dashboard production bundle exactly once (via ^build) + # then the backend binary with embedded dashboard. Ubuntu tag only; + # other tags are compile-checked in distro-tags. + - run: bun run build - packaging: - name: Packaging manifest and hook checks + packaging-validation: + name: Packaging validation runs-on: ubuntu-24.04 + timeout-minutes: 20 + env: + TAKO_DISTRO: ubuntu steps: + # Both setups are load-bearing: package:check probes PAM headers via + # cc -lpam and runs systemd-analyze verify (setup-linux-backend), and + # reads `go env` (CGO_ENABLED, GOARCH) plus `goreleaser check` (setup-go + # plus GoReleaser below). It compiles no Go code itself. - uses: actions/checkout@v4 - uses: ./.github/actions/setup-linux-backend - uses: actions/setup-go@v5 @@ -227,88 +296,14 @@ jobs: with: bun-version: ${{ env.BUN_VERSION }} - run: bun install --frozen-lockfile - - name: Install GoReleaser for static configuration checks - uses: goreleaser/goreleaser-action@v7 + - uses: goreleaser/goreleaser-action@v7 with: distribution: goreleaser version: "v${{ vars.GORELEASER_VERSION || '2.18.0' }}" install-only: true + # package:check runs goreleaser check, manifest cross-checks, + # systemd-analyze verify, plus mocked maintainer-script and host + # tests. Safe for PR CI. Full snapshot builds, native Docker + # package builds, and tools/tako-integration stay in release-only + # workflows. - run: bun run package:check - - run: bun test tools/release-gate.test.ts - - openapi-contract: - name: OpenAPI contract validation - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: ${{ env.BUN_VERSION }} - - run: bunx --bun @redocly/cli@1.34.0 lint --config .redocly.yaml apps/backend/api/openapi.yaml - - workflow-quality: - name: GitHub workflow validation - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version-file: apps/backend/go.mod - cache: true - cache-dependency-path: | - apps/backend/go.sum - go.work.sum - - name: Install actionlint - env: - GOBIN: ${{ runner.temp }}/bin - run: | - mkdir -p "$GOBIN" - go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 - echo "$GOBIN" >> "$GITHUB_PATH" - - run: actionlint - - shell-quality: - name: Shell validation - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - name: Install shellcheck - run: | - sudo apt-get update - sudo apt-get install --yes --no-install-recommends shellcheck - - name: Run shellcheck - run: >- - shellcheck --severity=error - tools/package - tools/package-check - tools/tako-go.sh - tools/tako-host-test - tools/tako-integration - apps/backend/packaging/*.sh - apps/backend/packaging/scripts/deb/*.sh - apps/backend/packaging/scripts/rpm/*.sh - apps/backend/packaging/scripts/arch/*.sh - - pr-gate: - name: PR gate - if: ${{ always() }} - needs: - - go-unit - - go-race - - go-quality - - dashboard-tests - - dashboard-quality - - dashboard-coverage - - web - - backend-build - - packaging - - openapi-contract - - workflow-quality - - shell-quality - runs-on: ubuntu-24.04 - steps: - - name: Fail if a required check failed, was cancelled, or skipped - if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') }} - run: exit 1 - - name: All required checks passed - run: echo "All required CI checks passed" From 3682a6c7a8ae8e270ec19494dd2ab4c3d8ef2dca Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sun, 20 Sep 2026 10:28:11 +0530 Subject: [PATCH 2/7] perf(ci): optimize Linux backend setup to skip redundant apt Only run apt-get update and install when gcc, libpam0g-dev, libsystemd-dev or systemd are actually missing, avoiding a redundant update on fresh runners where deps are preinstalled. --- .../actions/setup-linux-backend/action.yml | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/actions/setup-linux-backend/action.yml b/.github/actions/setup-linux-backend/action.yml index fd19057..27ded00 100644 --- a/.github/actions/setup-linux-backend/action.yml +++ b/.github/actions/setup-linux-backend/action.yml @@ -7,9 +7,17 @@ runs: shell: bash run: | set -euo pipefail - sudo apt-get update - sudo apt-get install --yes --no-install-recommends \ - gcc \ - libpam0g-dev \ - libsystemd-dev \ - systemd + # Only hit apt when something is actually missing. Each CI job runs + # this once on a fresh runner, so per-job cost is one update at most. + # Callers needing several distro tags must loop tags in a single job + # instead of adding a matrix that repeats this setup. + missing=() + for pkg in gcc libpam0g-dev libsystemd-dev systemd; do + if ! dpkg -s "$pkg" >/dev/null 2>&1; then + missing+=("$pkg") + fi + done + if [ "${#missing[@]}" -gt 0 ]; then + sudo apt-get update + sudo apt-get install --yes --no-install-recommends "${missing[@]}" + fi From dce45f4000e496e5c20ce56287623a5a4e035574 Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sun, 20 Sep 2026 10:28:13 +0530 Subject: [PATCH 3/7] chore(ci): remove disposable VM validation workflows Delete integration, packaging-lifecycle and vm-validation reusable workflows and the vm-test.sh evidence harness. These self-hosted tako-vm-* runners were an operator-provided prerequisite and are no longer a PR or release gate. --- .github/workflows/integration.yml | 100 --------------- .github/workflows/packaging-lifecycle.yml | 115 ----------------- .github/workflows/vm-validation.yml | 76 ----------- apps/backend/packaging/vm-test.sh | 146 ---------------------- 4 files changed, 437 deletions(-) delete mode 100644 .github/workflows/integration.yml delete mode 100644 .github/workflows/packaging-lifecycle.yml delete mode 100644 .github/workflows/vm-validation.yml delete mode 100755 apps/backend/packaging/vm-test.sh diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml deleted file mode 100644 index 688d5ec..0000000 --- a/.github/workflows/integration.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: Backend integration - -on: - workflow_call: - workflow_dispatch: - schedule: - - cron: "17 2 * * 1-5" - -permissions: - contents: read - -jobs: - integration: - name: ${{ matrix.distro }} disposable VM integration - strategy: - fail-fast: false - matrix: - include: - - distro: ubuntu - vm_label: tako-vm-ubuntu2604-amd64 - - distro: fedora - vm_label: tako-vm-fedora44-amd64 - runs-on: [self-hosted, linux, "${{ matrix.vm_label }}"] - timeout-minutes: 30 - env: - TAKO_INTEGRATION_DISTRO: ${{ matrix.distro }} - TAKO_INTEGRATION_EVIDENCE_DIR: integration-evidence/${{ matrix.distro }} - GOCACHE: /tmp/tako-go-integration-cache - GOMODCACHE: /tmp/tako-go-integration-mod-cache - TAKO_TEST_PAM_USER: tako-ci - TAKO_TEST_BINARY: ${{ github.workspace }}/bin/tako-integration - TAKO_TEST_ACCOUNT_VM: "1" - TAKO_TEST_IDENTITY_VM: "1" - TAKO_TEST_JOURNAL_VM: "1" - TAKO_TEST_LOGIN_HISTORY_USER: tako-ci - TAKO_TEST_PROCESS_VM: "1" - TAKO_TEST_OVERRIDE_VM: "1" - TAKO_TEST_SSH_HOME: /home/tako-ci - TAKO_TEST_SSH_USER: tako-ci - TAKO_TEST_TIMER_VM: "1" - TAKO_TEST_PASSWORD_USER: tako-ci - TAKO_TEST_PASSWORD_SERVICE: passwd - TAKO_TEST_PAM_SERVICE: tako - steps: - - uses: actions/checkout@v4 - - name: Install backend prerequisites - run: | - set -euo pipefail - case "$TAKO_INTEGRATION_DISTRO" in - ubuntu) - sudo apt-get update - sudo apt-get install --yes --no-install-recommends \ - gcc libpam0g-dev libsystemd-dev systemd openssl python3 - ;; - fedora) - sudo dnf install --assumeyes \ - gcc pam-devel systemd systemd-devel openssl python3 - ;; - esac - - uses: actions/setup-go@v5 - with: - go-version-file: apps/backend/go.mod - cache: true - cache-dependency-path: | - apps/backend/go.sum - go.work.sum - - name: Generate disposable PAM password fixture - run: | - set -euo pipefail - old_password="tako-ci-$(openssl rand -hex 16)" - new_password="tako-ci-$(openssl rand -hex 16)" - echo "::add-mask::$old_password" - echo "::add-mask::$new_password" - { - printf 'TAKO_TEST_PASSWORD_OLD=%s\n' "$old_password" - printf 'TAKO_TEST_PASSWORD_NEW=%s\n' "$new_password" - printf 'TAKO_TEST_PAM_RESPONSES=["%s"]\n' "$old_password" - } >> "$GITHUB_ENV" - - name: Build integration binary - working-directory: apps/backend - env: - TAKO_DISTRO: ${{ matrix.distro }} - run: | - set -euo pipefail - mkdir -p "$GITHUB_WORKSPACE/bin" - go build -tags "$TAKO_DISTRO" -buildvcs=false \ - -o "$GITHUB_WORKSPACE/bin/tako-integration" ./cmd/tako - - name: Run tagged integration suite and VM preflight - run: | - set -euo pipefail - sudo --preserve-env=PATH,GOCACHE,GOMODCACHE,TAKO_INTEGRATION_DISTRO,TAKO_INTEGRATION_EVIDENCE_DIR,TAKO_TEST_BINARY,TAKO_TEST_PAM_USER,TAKO_TEST_PAM_RESPONSES,TAKO_TEST_PASSWORD_OLD,TAKO_TEST_PASSWORD_NEW,TAKO_TEST_ACCOUNT_VM,TAKO_TEST_IDENTITY_VM,TAKO_TEST_JOURNAL_VM,TAKO_TEST_LOGIN_HISTORY_USER,TAKO_TEST_PROCESS_VM,TAKO_TEST_OVERRIDE_VM,TAKO_TEST_SSH_HOME,TAKO_TEST_SSH_USER,TAKO_TEST_TIMER_VM,TAKO_TEST_PASSWORD_USER,TAKO_TEST_PASSWORD_SERVICE,TAKO_TEST_PAM_SERVICE \ - "$GITHUB_WORKSPACE/tools/tako-integration" - - name: Upload integration logs and diagnostics - if: ${{ always() }} - uses: actions/upload-artifact@v4 - with: - name: integration-evidence-${{ matrix.distro }} - path: integration-evidence/${{ matrix.distro }}/ - if-no-files-found: warn - retention-days: 30 diff --git a/.github/workflows/packaging-lifecycle.yml b/.github/workflows/packaging-lifecycle.yml deleted file mode 100644 index eb0e13b..0000000 --- a/.github/workflows/packaging-lifecycle.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Packaging lifecycle - -on: - workflow_call: - workflow_dispatch: - schedule: - - cron: "37 3 * * 1-5" - -permissions: - contents: read - -jobs: - build: - name: ${{ matrix.target }} old and new packages - runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - include: - - target: ubuntu2604-amd64 - arch: amd64 - image: ubuntu:26.04 - - target: fedora44-amd64 - arch: amd64 - image: fedora:44 - env: - TARGET_ID: ${{ matrix.target }} - TARGET_ARCH: ${{ matrix.arch }} - TARGET_IMAGE: ${{ matrix.image }} - steps: - - uses: actions/checkout@v4 - - name: Build old lifecycle package - run: | - set -euo pipefail - docker run --rm --platform "linux/${TARGET_ARCH}" \ - --volume "$GITHUB_WORKSPACE:/workspace" \ - --workdir /workspace \ - --env TAKO_PACKAGE_TARGET="$TARGET_ID" \ - --env GORELEASER_CURRENT_TAG="v0.0.1-ci.1.${GITHUB_RUN_ID}" \ - "$TARGET_IMAGE" \ - /bin/bash -c './apps/backend/packaging/native-build.sh' - package=$(find "$GITHUB_WORKSPACE/dist" -maxdepth 1 -type f \( \ - -name '*.deb' -o -name '*.rpm' -o -name '*.pkg.tar.zst' \ - \) -print -quit) - test -n "$package" - artifact_dir="$RUNNER_TEMP/lifecycle-artifacts/$TARGET_ID" - mkdir -p "$artifact_dir" - cp "$package" "$artifact_dir/old-$(basename "$package")" - - name: Build new lifecycle package - run: | - set -euo pipefail - docker run --rm --platform "linux/${TARGET_ARCH}" \ - --volume "$GITHUB_WORKSPACE:/workspace" \ - --workdir /workspace \ - --env TAKO_PACKAGE_TARGET="$TARGET_ID" \ - --env GORELEASER_CURRENT_TAG="v0.0.1-ci.2.${GITHUB_RUN_ID}" \ - "$TARGET_IMAGE" \ - /bin/bash -c './apps/backend/packaging/native-build.sh' - package=$(find "$GITHUB_WORKSPACE/dist" -maxdepth 1 -type f \( \ - -name '*.deb' -o -name '*.rpm' -o -name '*.pkg.tar.zst' \ - \) -print -quit) - test -n "$package" - artifact_dir="$RUNNER_TEMP/lifecycle-artifacts/$TARGET_ID" - mkdir -p "$artifact_dir" - cp "$package" "$artifact_dir/new-$(basename "$package")" - - uses: actions/upload-artifact@v4 - with: - name: lifecycle-packages-${{ matrix.target }} - path: ${{ runner.temp }}/lifecycle-artifacts/${{ matrix.target }}/ - if-no-files-found: error - retention-days: 14 - - validate: - name: ${{ matrix.target }} lifecycle VM - needs: build - strategy: - fail-fast: false - matrix: - include: - - target: ubuntu2604-amd64 - vm_label: tako-vm-ubuntu2604-amd64 - - target: fedora44-amd64 - vm_label: tako-vm-fedora44-amd64 - runs-on: [self-hosted, linux, "${{ matrix.vm_label }}"] - timeout-minutes: 45 - env: - TARGET_ID: ${{ matrix.target }} - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: lifecycle-packages-${{ matrix.target }} - path: lifecycle-packages - - name: Run lifecycle validation in disposable VM - run: | - set -euo pipefail - old_package=$(find lifecycle-packages -type f -name 'old-*' -print -quit) - new_package=$(find lifecycle-packages -type f -name 'new-*' -print -quit) - test -n "$old_package" - test -n "$new_package" - sudo env \ - GITHUB_SHA="$GITHUB_SHA" \ - TAKO_VM_TARGET="$TARGET_ID" \ - TAKO_VM_PACKAGE="$GITHUB_WORKSPACE/$new_package" \ - TAKO_VM_OLD_PACKAGE="$GITHUB_WORKSPACE/$old_package" \ - TAKO_VM_EVIDENCE_DIR="$GITHUB_WORKSPACE/lifecycle-evidence/$TARGET_ID" \ - TAKO_VM_MANIFEST="$GITHUB_WORKSPACE/apps/backend/packaging/targets.json" \ - "$GITHUB_WORKSPACE/apps/backend/packaging/vm-test.sh" - - uses: actions/upload-artifact@v4 - if: ${{ always() }} - with: - name: lifecycle-evidence-${{ matrix.target }} - path: lifecycle-evidence/${{ matrix.target }}/ - if-no-files-found: warn - retention-days: 30 diff --git a/.github/workflows/vm-validation.yml b/.github/workflows/vm-validation.yml deleted file mode 100644 index 3a36bc7..0000000 --- a/.github/workflows/vm-validation.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Packaging VM validation - -on: - workflow_call: - -permissions: - contents: read - actions: read - -jobs: - manifest: - name: Expand VM target manifest - runs-on: ubuntu-24.04 - outputs: - matrix: ${{ steps.targets.outputs.matrix }} - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: 1.4.0 - - id: targets - run: | - matrix=$(bun -e ' - const manifest = await Bun.file("apps/backend/packaging/targets.json").json(); - console.log(JSON.stringify({include: manifest.targets.map((target) => ({ - id: target.id, - arch: target.arch, - format: target.package_format, - vm_label: target.vm_label, - }))})); - ') - echo "matrix=$matrix" >> "$GITHUB_OUTPUT" - - validate: - name: ${{ matrix.id }} disposable VM - needs: manifest - strategy: - fail-fast: false - matrix: ${{ fromJSON(needs.manifest.outputs.matrix) }} - # These labels are an explicit operator prerequisite. A beta release stays - # blocked while a target VM runner is missing or offline. - runs-on: [self-hosted, linux, "${{ matrix.vm_label }}"] - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - name: Download native package - uses: actions/download-artifact@v4 - with: - name: package-${{ matrix.id }} - path: package - - name: Run installed-system smoke validation - env: - TARGET_ID: ${{ matrix.id }} - PACKAGE_FORMAT: ${{ matrix.format }} - run: | - case "$PACKAGE_FORMAT" in - deb) package=$(find package -type f -name '*.deb' -print -quit) ;; - rpm) package=$(find package -type f -name '*.rpm' -print -quit) ;; - archlinux) package=$(find package -type f -name '*.pkg.tar.zst' -print -quit) ;; - *) echo "unknown package format: $PACKAGE_FORMAT" >&2; exit 2 ;; - esac - test -n "$package" - sudo env \ - GITHUB_SHA="$GITHUB_SHA" \ - TAKO_VM_TARGET="$TARGET_ID" \ - TAKO_VM_PACKAGE="$GITHUB_WORKSPACE/$package" \ - TAKO_VM_EVIDENCE_DIR="$GITHUB_WORKSPACE/vm-evidence" \ - TAKO_VM_MANIFEST="$GITHUB_WORKSPACE/apps/backend/packaging/targets.json" \ - "$GITHUB_WORKSPACE/apps/backend/packaging/vm-test.sh" - - name: Upload VM evidence - uses: actions/upload-artifact@v4 - with: - name: vm-evidence-${{ matrix.id }} - path: vm-evidence/ - if-no-files-found: error - retention-days: 30 diff --git a/apps/backend/packaging/vm-test.sh b/apps/backend/packaging/vm-test.sh deleted file mode 100755 index 6ed3702..0000000 --- a/apps/backend/packaging/vm-test.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -# This is the only script that writes a VM validation result. It is intended to -# run as root in a disposable, booted target VM. The release workflow consumes -# the result and refuses to publish when a target has no passing evidence. -target_id="${TAKO_VM_TARGET:-}" -new_package="${TAKO_VM_PACKAGE:-}" -old_package="${TAKO_VM_OLD_PACKAGE:-}" -evidence_dir="${TAKO_VM_EVIDENCE_DIR:-vm-evidence}" -manifest="${TAKO_VM_MANIFEST:-apps/backend/packaging/targets.json}" -evidence_file="$evidence_dir/${target_id:-unknown}.json" -status=failed -started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) -log_file="" - -write_evidence() { - local exit_code=$? - mkdir -p "$evidence_dir" - python3 - "$evidence_file" "$target_id" "$status" "$exit_code" "$started_at" "$new_package" "$old_package" "$log_file" <<'PY' -import hashlib -import json -import os -import platform -import sys -from datetime import datetime, timezone - -path, target, status, exit_code, started, new_package, old_package, log_file = sys.argv[1:] - -def digest(value): - if not value: - return None - try: - with open(value, "rb") as stream: - return hashlib.sha256(stream.read()).hexdigest() - except OSError: - return None - -payload = { - "schema": 1, - "target": target, - "status": status, - "exit_code": int(exit_code), - "started_at": started, - "finished_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"), - "commit": os.environ.get("GITHUB_SHA") or os.environ.get("TAKO_VM_COMMIT"), - "host": { - "machine": platform.machine(), - "system": platform.system(), - "release": platform.release(), - }, - "package": { - "new": os.path.basename(new_package) if new_package else None, - "new_sha256": digest(new_package), - "old": os.path.basename(old_package) if old_package else None, - "old_sha256": digest(old_package), - }, - "log": os.path.basename(log_file) if log_file else None, -} -with open(path, "w", encoding="utf-8") as stream: - json.dump(payload, stream, sort_keys=True, indent=2) - stream.write("\n") -PY - return "$exit_code" -} -trap write_evidence EXIT - -if [[ "$EUID" -ne 0 ]]; then - echo "vm-test: run as root in a disposable target VM" >&2 - exit 2 -fi -if [[ -z "$target_id" || -z "$new_package" ]]; then - echo "usage: TAKO_VM_TARGET=TARGET TAKO_VM_PACKAGE=PACKAGE $0" >&2 - exit 2 -fi -if [[ ! -f "$manifest" || ! -f "$new_package" ]]; then - echo "vm-test: manifest and package must be present in the VM" >&2 - exit 2 -fi - -mkdir -p "$evidence_dir" -log_file="$evidence_dir/${target_id}.log" -: >"$log_file" -exec > >(tee -a "$log_file") 2>&1 - -target_json=$(python3 - "$manifest" "$target_id" <<'PY' -import json -import sys -with open(sys.argv[1], encoding="utf-8") as stream: - manifest = json.load(stream) -for target in manifest["targets"]: - if target["id"] == sys.argv[2]: - print(json.dumps(target)) - break -else: - raise SystemExit("unknown target") -PY -) - -read_target() { - python3 -c 'import json,sys; print(json.loads(sys.argv[1])[sys.argv[2]])' "$target_json" "$1" -} - -expected_distro=$(read_target distribution) -expected_release=$(read_target release) -expected_arch=$(read_target goarch) -package_format=$(read_target package_format) -host_arch=$(go env GOARCH 2>/dev/null || true) -[[ -n "$host_arch" ]] || host_arch=$(case "$(uname -m)" in x86_64) echo amd64;; aarch64) echo arm64;; *) echo unknown;; esac) -[[ "$host_arch" == "$expected_arch" ]] || { echo "vm-test: host arch $host_arch does not match $expected_arch" >&2; exit 1; } - -declare -A os_release=() -while IFS='=' read -r key value; do - value=${value%\"} - value=${value#\"} - os_release["$key"]="$value" -done < /etc/os-release -host_distro=${os_release[ID]:-} -case "$host_distro" in - arch) host_distro=archlinux ;; - opensuse-leap|opensuse-tumbleweed|sles) host_distro=opensuse ;; -esac -[[ "$host_distro" == "$expected_distro" ]] || { echo "vm-test: host distro $host_distro does not match $expected_distro" >&2; exit 1; } -if [[ "$expected_release" != rolling ]]; then - host_release=${os_release[VERSION_ID]:-} - [[ "$host_release" == "$expected_release" || "$host_release" == "$expected_release."* ]] || { - echo "vm-test: host release $host_release does not match $expected_release" >&2 - exit 1 - } -fi - -if [[ -n "$old_package" ]]; then - [[ -f "$old_package" ]] || { echo "vm-test: old package does not exist" >&2; exit 1; } - TAKO_DISPOSABLE_HOST=1 apps/backend/packaging/lifecycle-test.sh "$old_package" "$new_package" -else - case "$package_format" in - deb) dpkg --install "$new_package" ;; - rpm) rpm --upgrade --verbose --hash "$new_package" ;; - archlinux) pacman --noconfirm --upgrade "$new_package" ;; - *) echo "vm-test: unsupported package format $package_format" >&2; exit 1 ;; - esac - TAKO_BINARY=/usr/bin/tako apps/backend/packaging/smoke-test.sh -fi - -status=passed -echo "VM validation passed for $target_id" From 7b509dfa97cabe9d5ca2f3f15b798f32d5ab5246 Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sun, 20 Sep 2026 10:28:16 +0530 Subject: [PATCH 4/7] chore(release): remove VM evidence gate and dependency inventory tooling Delete release-gate, its tests and release-dependencies. The evidence digest/commit matching and dependency-inventory.json generation were tied to the beta evidence-gate and Sigstore signing that are no longer used. --- tools/release-dependencies | 61 ----------------------------------- tools/release-gate | 58 ---------------------------------- tools/release-gate.test.ts | 65 -------------------------------------- 3 files changed, 184 deletions(-) delete mode 100755 tools/release-dependencies delete mode 100755 tools/release-gate delete mode 100644 tools/release-gate.test.ts diff --git a/tools/release-dependencies b/tools/release-dependencies deleted file mode 100755 index 21aabe1..0000000 --- a/tools/release-dependencies +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bun - -const output = process.argv[2] ?? "dependency-inventory.json"; -const run = (command, cwd = ".") => { - const result = Bun.spawnSync({ cmd: command, cwd, stdout: "pipe", stderr: "pipe" }); - if (result.exitCode !== 0) throw new Error(`${command.join(" ")} failed: ${result.stderr.toString()}`); - return result.stdout.toString(); -}; - -const moduleText = run(["go", "list", "-m", "-json", "all"], "apps/backend"); -const moduleRecords = []; -let start = -1; -let depth = 0; -let quoted = false; -let escaped = false; -for (let index = 0; index < moduleText.length; index += 1) { - const character = moduleText[index]; - if (quoted) { - if (escaped) escaped = false; - else if (character === "\\") escaped = true; - else if (character === '"') quoted = false; - continue; - } - if (character === '"') { - quoted = true; - continue; - } - if (character === "{") { - if (depth === 0) start = index; - depth += 1; - } else if (character === "}") { - depth -= 1; - if (depth === 0 && start >= 0) { - moduleRecords.push(JSON.parse(moduleText.slice(start, index + 1))); - start = -1; - } - } -} -const modules = moduleRecords - .map(({ Path, Version, Sum, GoMod, GoModSum, Main }) => ({ - path: Path, - version: Version ?? null, - sum: Sum ?? null, - go_mod: GoMod ?? null, - go_mod_sum: GoModSum ?? null, - main: Boolean(Main), - })); - -const lockfile = Bun.file("bun.lock"); -const lockBytes = await lockfile.arrayBuffer(); -const lockHash = new Bun.CryptoHasher("sha256").update(lockBytes).digest("hex"); -const goVersion = run(["go", "version"]).trim(); -const inventory = { - schema: 1, - generated_at: new Date().toISOString(), - commit: process.env.GITHUB_SHA ?? null, - go: { version: goVersion, modules }, - javascript: { lockfile: "bun.lock", lockfile_sha256: lockHash }, -}; -await Bun.write(output, `${JSON.stringify(inventory, null, 2)}\n`); -console.log(`dependency inventory written to ${output}`); diff --git a/tools/release-gate b/tools/release-gate deleted file mode 100755 index 2989a27..0000000 --- a/tools/release-gate +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bun - -const manifestPath = process.env.TAKO_RELEASE_MANIFEST ?? "apps/backend/packaging/targets.json"; -const evidenceDir = process.argv[2] ?? process.env.TAKO_VM_EVIDENCE_DIR ?? "vm-evidence"; -const packageDir = process.argv[3] ?? process.env.TAKO_RELEASE_PACKAGE_DIR ?? ""; -const expectedCommit = process.argv[4] ?? process.env.TAKO_VM_COMMIT ?? process.env.GITHUB_SHA ?? ""; - -const fail = (message) => { - console.error(`release gate: ${message}`); - process.exit(1); -}; - -const manifest = await Bun.file(manifestPath).json().catch(() => null); -if (!manifest || !Array.isArray(manifest.targets) || manifest.targets.length === 0) fail(`cannot read targets from ${manifestPath}`); - -const targetIds = new Set(); -for (const target of manifest.targets) { - if (typeof target.id !== "string" || target.id === "" || targetIds.has(target.id)) fail(`manifest contains an invalid or duplicate target id: ${target.id}`); - targetIds.add(target.id); -} - -if ((await Bun.$`test -d ${evidenceDir}`.quiet().nothrow()).exitCode !== 0) fail(`evidence directory does not exist: ${evidenceDir}`); -if (packageDir && (await Bun.$`test -d ${packageDir}`.quiet().nothrow()).exitCode !== 0) fail(`package directory does not exist: ${packageDir}`); - -const evidence = new Map(); -for await (const path of new Bun.Glob("**/*.json").scan({ cwd: evidenceDir, absolute: true })) { - const value = await Bun.file(path).json().catch(() => null); - if (!value || typeof value.target !== "string") continue; - if (evidence.has(value.target)) fail(`duplicate evidence for ${value.target}`); - evidence.set(value.target, value); -} - -for (const target of manifest.targets) { - const value = evidence.get(target.id); - if (!value) fail(`missing VM evidence for ${target.id}`); - if (value.status !== "passed") fail(`${target.id} did not pass (${value.status ?? "missing status"})`); - if (value.exit_code !== 0) fail(`${target.id} reported exit code ${value.exit_code}`); - if (expectedCommit && value.commit !== expectedCommit) fail(`${target.id} evidence is for ${value.commit ?? "an unknown commit"}, expected ${expectedCommit}`); - if (!value.package?.new_sha256) fail(`${target.id} evidence does not identify the tested package digest`); - if (packageDir) { - const suffix = { - deb: ".deb", - rpm: ".rpm", - archlinux: ".pkg.tar.zst", - }[target.package_format]; - const candidates = []; - for await (const path of new Bun.Glob("**/*").scan({ cwd: packageDir, absolute: true, onlyFiles: true })) { - if (path.endsWith(`_${target.package_name}${suffix}`)) candidates.push(path); - } - if (candidates.length !== 1) fail(`${target.id} has ${candidates.length} assembled package candidates`); - const hash = new Bun.CryptoHasher("sha256").update(await Bun.file(candidates[0]).arrayBuffer()).digest("hex"); - if (hash !== value.package.new_sha256) fail(`${target.id} evidence digest does not match the assembled package`); - } -} - -for (const [target] of evidence) if (!targetIds.has(target)) fail(`evidence contains an unknown target: ${target}`); - -console.log(`release gate passed: ${targetIds.size} manifest targets have passing VM evidence`); diff --git a/tools/release-gate.test.ts b/tools/release-gate.test.ts deleted file mode 100644 index cbed3e7..0000000 --- a/tools/release-gate.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { afterEach, expect, test } from "bun:test"; -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -const directories: string[] = []; -afterEach(async () => { - await Promise.all(directories.splice(0).map((path) => rm(path, { recursive: true, force: true }))); -}); - -async function fixture() { - const root = await mkdtemp(join(tmpdir(), "tako-release-gate-")); - directories.push(root); - const manifest = join(root, "manifest.json"); - const evidence = join(root, "evidence", "target.json"); - const artifact = join(root, "packages", "tako_1.0_ubuntu_amd64.deb"); - await Bun.write(manifest, JSON.stringify({ targets: [{ - id: "ubuntu-amd64", package_name: "ubuntu_amd64", package_format: "deb", - }] })); - await Bun.write(artifact, "tested package"); - const record = { - target: "ubuntu-amd64", status: "passed", exit_code: 0, commit: "tested-commit", - package: { new_sha256: new Bun.CryptoHasher("sha256").update("tested package").digest("hex") }, - }; - await Bun.write(evidence, JSON.stringify(record)); - return { root, manifest, evidence, artifact, record }; -} - -async function gate(value: Awaited>) { - const result = Bun.spawnSync({ - cmd: [process.execPath, join(import.meta.dir, "release-gate"), - join(value.root, "evidence"), join(value.root, "packages"), "tested-commit"], - env: { ...process.env, TAKO_RELEASE_MANIFEST: value.manifest }, - stdout: "pipe", stderr: "pipe", - }); - return { code: result.exitCode, error: result.stderr.toString() }; -} - -test("accepts the exact package validated by the VM", async () => { - expect((await gate(await fixture())).code).toBe(0); -}); - -test("rejects a substituted release package", async () => { - const value = await fixture(); - await Bun.write(value.artifact, "untested package"); - const result = await gate(value); - expect(result.code).toBe(1); - expect(result.error).toContain("evidence digest does not match"); -}); - -test("rejects evidence from another commit", async () => { - const value = await fixture(); - await Bun.write(value.evidence, JSON.stringify({ ...value.record, commit: "old-commit" })); - const result = await gate(value); - expect(result.code).toBe(1); - expect(result.error).toContain("expected tested-commit"); -}); - -test("rejects missing VM evidence", async () => { - const value = await fixture(); - await rm(value.evidence); - const result = await gate(value); - expect(result.code).toBe(1); - expect(result.error).toContain("missing VM evidence"); -}); From d71a3cfa91e49a9b42de321bc2b093330c829da4 Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sun, 20 Sep 2026 10:28:18 +0530 Subject: [PATCH 5/7] refactor(packaging): remove vm_label runner contract Strip vm_label from all 15 targets in the packaging manifest and drop it from package-check validation. VM labels were the operator-provided self-hosted runner contract for disposable VM validation, now removed. --- apps/backend/packaging/targets.json | 45 ++++++++++------------------- tools/package-check | 2 +- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/apps/backend/packaging/targets.json b/apps/backend/packaging/targets.json index 49d051d..c67ae56 100644 --- a/apps/backend/packaging/targets.json +++ b/apps/backend/packaging/targets.json @@ -35,8 +35,7 @@ "package_format": "deb", "package_name": "debian_amd64", "pam_source": "tako.debian", - "branding": "debian", - "vm_label": "tako-vm-debian13-amd64" + "branding": "debian" }, { "id": "debian13-arm64", @@ -52,8 +51,7 @@ "package_format": "deb", "package_name": "debian_arm64", "pam_source": "tako.debian", - "branding": "debian", - "vm_label": "tako-vm-debian13-arm64" + "branding": "debian" }, { "id": "ubuntu2604-amd64", @@ -69,8 +67,7 @@ "package_format": "deb", "package_name": "ubuntu_amd64", "pam_source": "tako.debian", - "branding": "ubuntu", - "vm_label": "tako-vm-ubuntu2604-amd64" + "branding": "ubuntu" }, { "id": "ubuntu2604-arm64", @@ -86,8 +83,7 @@ "package_format": "deb", "package_name": "ubuntu_arm64", "pam_source": "tako.debian", - "branding": "ubuntu", - "vm_label": "tako-vm-ubuntu2604-arm64" + "branding": "ubuntu" }, { "id": "fedora44-amd64", @@ -102,8 +98,7 @@ "package_format": "rpm", "package_name": "fedora_amd64", "pam_source": "tako.redhat", - "branding": "fedora", - "vm_label": "tako-vm-fedora44-amd64" + "branding": "fedora" }, { "id": "fedora44-arm64", @@ -118,8 +113,7 @@ "package_format": "rpm", "package_name": "fedora_arm64", "pam_source": "tako.redhat", - "branding": "fedora", - "vm_label": "tako-vm-fedora44-arm64" + "branding": "fedora" }, { "id": "rhel102-amd64", @@ -134,8 +128,7 @@ "package_format": "rpm", "package_name": "rhel_amd64", "pam_source": "tako.redhat", - "branding": "rhel", - "vm_label": "tako-vm-rhel102-amd64" + "branding": "rhel" }, { "id": "rhel102-arm64", @@ -150,8 +143,7 @@ "package_format": "rpm", "package_name": "rhel_arm64", "pam_source": "tako.redhat", - "branding": "rhel", - "vm_label": "tako-vm-rhel102-arm64" + "branding": "rhel" }, { "id": "rocky102-amd64", @@ -166,8 +158,7 @@ "package_format": "rpm", "package_name": "rocky_amd64", "pam_source": "tako.redhat", - "branding": "rocky", - "vm_label": "tako-vm-rocky102-amd64" + "branding": "rocky" }, { "id": "rocky102-arm64", @@ -182,8 +173,7 @@ "package_format": "rpm", "package_name": "rocky_arm64", "pam_source": "tako.redhat", - "branding": "rocky", - "vm_label": "tako-vm-rocky102-arm64" + "branding": "rocky" }, { "id": "almalinux102-amd64", @@ -198,8 +188,7 @@ "package_format": "rpm", "package_name": "almalinux_amd64", "pam_source": "tako.redhat", - "branding": "almalinux", - "vm_label": "tako-vm-almalinux102-amd64" + "branding": "almalinux" }, { "id": "almalinux102-arm64", @@ -214,8 +203,7 @@ "package_format": "rpm", "package_name": "almalinux_arm64", "pam_source": "tako.redhat", - "branding": "almalinux", - "vm_label": "tako-vm-almalinux102-arm64" + "branding": "almalinux" }, { "id": "opensuse160-amd64", @@ -231,8 +219,7 @@ "package_format": "rpm", "package_name": "opensuse_amd64", "pam_source": "tako.opensuse", - "branding": "opensuse", - "vm_label": "tako-vm-opensuse160-amd64" + "branding": "opensuse" }, { "id": "opensuse160-arm64", @@ -248,8 +235,7 @@ "package_format": "rpm", "package_name": "opensuse_arm64", "pam_source": "tako.opensuse", - "branding": "opensuse", - "vm_label": "tako-vm-opensuse160-arm64" + "branding": "opensuse" }, { "id": "arch-rolling-amd64", @@ -264,8 +250,7 @@ "package_format": "archlinux", "package_name": "archlinux_amd64", "pam_source": "tako", - "branding": "archlinux", - "vm_label": "tako-vm-arch-rolling-amd64" + "branding": "archlinux" } ] } diff --git a/tools/package-check b/tools/package-check index 1d52ef4..9e809e4 100755 --- a/tools/package-check +++ b/tools/package-check @@ -53,7 +53,7 @@ const config = Bun.YAML.parse(await Bun.file(".goreleaser.yaml").text()); const builds = new Map((config.builds ?? []).map((build) => [build.id, build])); const packageIds = new Set((config.nfpms ?? []).map((pkg) => pkg.id)); for (const target of targets) { - for (const key of ["id", "distribution", "release", "image", "arch", "goarch", "build_tag", "build_id", "package_id", "package_format", "package_name", "pam_source", "branding", "vm_label"]) { + for (const key of ["id", "distribution", "release", "image", "arch", "goarch", "build_tag", "build_id", "package_id", "package_format", "package_name", "pam_source", "branding"]) { if (typeof target[key] !== "string" || target[key] === "") throw new Error(`${target.id ?? "target"} is missing ${key}`); } if (ids.has(target.id)) throw new Error(`duplicate target id: ${target.id}`); From 69374c85ff5ab2c006893aa87af51c95e723b794 Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sun, 20 Sep 2026 10:28:22 +0530 Subject: [PATCH 6/7] docs(packaging): describe simplified release without VM gates Update README and RELEASE to reference the new release.yml flow (tag suffix determines prerelease vs stable, no VM evidence gate, signing or provenance). Remove references to the beta matrix, VM evidence harness and lifecycle-test evidence recording. --- apps/backend/packaging/README.md | 6 ++--- apps/backend/packaging/RELEASE.md | 37 ++++++++++++------------------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/apps/backend/packaging/README.md b/apps/backend/packaging/README.md index 54ee009..6ea6b33 100644 --- a/apps/backend/packaging/README.md +++ b/apps/backend/packaging/README.md @@ -29,9 +29,9 @@ Password changes and administrative resets use the host's standard `passwd` PAM ## GoReleaser packages -Local packaging requires installed Bun dependencies, Go 1.26+, GoReleaser 2.x, a C compiler, and the host's PAM development headers and linker files. `bun run package:check` validates those capabilities without assuming a distribution package manager. The default package command cross-packages every manifest distro target matching the host architecture; use `TAKO_PACKAGE_TARGET` to select one native manifest entry. The complete beta matrix and its external VM prerequisites are documented in [`RELEASE.md`](RELEASE.md). +Local packaging requires installed Bun dependencies, Go 1.26+, GoReleaser 2.x, a C compiler, and the host's PAM development headers and linker files. `bun run package:check` validates those capabilities without assuming a distribution package manager. The default package command cross-packages every manifest distro target matching the host architecture; use `TAKO_PACKAGE_TARGET` to select one native manifest entry. The complete matrix is documented in [`RELEASE.md`](RELEASE.md). -Run `bun run package` from repository root to validate packaging and emit one package and binary per manifest distro for the host architecture, plus `artifacts.json` and `checksums.txt` in `dist/`. `TAKO_PACKAGE_TARGET= bun run package` emits one native package and binary. GoReleaser's embedded nFPM packager creates packages without requiring a separate package-builder tool. Snapshot packages do not publish releases. The beta workflow builds each manifest target in its own target image, signs the final checksum manifest, records a dependency inventory, and emits provenance only after the CI and VM evidence gates pass. Local cross-packages use the host's CGO, glibc, and PAM toolchain; native matrix builds provide target-runtime compatibility validation. +Run `bun run package` from repository root to validate packaging and emit one package and binary per manifest distro for the host architecture, plus `artifacts.json` and `checksums.txt` in `dist/`. `TAKO_PACKAGE_TARGET= bun run package` emits one native package and binary. GoReleaser's embedded nFPM packager creates packages without requiring a separate package-builder tool. Snapshot packages do not publish releases. The `release.yml` workflow builds each manifest target in its own target image on GitHub-hosted runners and publishes a GitHub release only after CI and every native package build pass; a hyphenated tag suffix marks it prerelease, a clean version marks it stable. Local cross-packages use the host's CGO, glibc, and PAM toolchain; native matrix builds provide target-runtime compatibility validation. Generated packages install full production runtime files: executable, four systemd units, sysusers and tmpfiles definitions, Polkit policy, the example TOML configuration under `/usr/share/doc/tako/`, exactly one matching distro branding asset under `/usr/share/tako/branding/`, and a distribution-specific PAM stack at `/etc/pam.d/tako`. Debian and Ubuntu use `pam/tako.debian`; Fedora, RHEL, Rocky, and AlmaLinux use `pam/tako.redhat`; openSUSE uses `pam/tako.opensuse`; Arch Linux uses the generic `pam/tako` policy. PAM files are package-managed as `config|noreplace`. Packages do not install a live `/etc/tako/config.toml`, sudoers example, smoke test, or operational README. @@ -76,4 +76,4 @@ Host-integrated development (`tools/tako-host`, dashboard overlay) is documented The installed-system smoke seam is `packaging/smoke-test.sh`. It checks binary presence, socket activation, static service enablement, service identities, TLS reachability, and (when `TAKO_SMOKE_USER`/`TAKO_SMOKE_PASSWORD` are provided) a real PAM login. `TAKO_SMOKE_USER` must be a **non-root** UNIX account; root-only success hides the user-bridge spawn path. It stops activated service processes at completion while leaving sockets running. -Run `packaging/lifecycle-test.sh OLD_PACKAGE NEW_PACKAGE` only inside a disposable VM with `TAKO_DISPOSABLE_HOST=1`. It destructively exercises fresh install, active upgrade, disabled/stopped preservation, administrator masks, removal, reinstall, purge, and trigger-limit health for two `.deb`, two `.rpm`, or two Arch `.pkg.tar.zst` files. Dependencies must already be available in the VM. `packaging/vm-test.sh` records evidence only after this harness or the installed-system smoke test actually succeeds. +Run `packaging/lifecycle-test.sh OLD_PACKAGE NEW_PACKAGE` only inside a disposable VM with `TAKO_DISPOSABLE_HOST=1`. It destructively exercises fresh install, active upgrade, disabled/stopped preservation, administrator masks, removal, reinstall, purge, and trigger-limit health for two `.deb`, two `.rpm`, or two Arch `.pkg.tar.zst` files. Dependencies must already be available in the VM. diff --git a/apps/backend/packaging/RELEASE.md b/apps/backend/packaging/RELEASE.md index 7d8749e..f0f8910 100644 --- a/apps/backend/packaging/RELEASE.md +++ b/apps/backend/packaging/RELEASE.md @@ -1,6 +1,6 @@ -# Beta release process +# Release process -`targets.json` is the source of truth for the beta support matrix. It currently +`targets.json` is the source of truth for the support matrix. It currently contains the 15 native combinations below: | Family | Release | Architectures | @@ -45,27 +45,18 @@ The `native-packages.yml` workflow expands the manifest and builds each target in its declared image. The arm64 entries require an arm64 GitHub runner; an amd64 runner cannot silently satisfy them. The RHEL image is the public UBI image named by the manifest. If an image tag or hosted runner is unavailable, -the target remains pending or fails and the beta release stays blocked. - -The reusable `vm-validation.yml` workflow consumes native artifacts from the -same beta run (use the beta workflow for manual end-to-end validation). It runs `packaging/vm-test.sh` on a disposable -VM runner carrying the `vm_label` from the manifest. These labels describe an -operator-provided runner contract; this repository does not contain VM hosts, -credentials, or evidence that a VM has run. Each successful invocation writes -one release input record containing the tested package digest. A failed or -missing record is a hard failure. - -PR and push CI run on GitHub-hosted runners. Privileged VM integration runs -on its schedule, by manual dispatch, and as a required beta release gate; it -is not a PR gate because it requires operator-provided disposable runners. - -The beta workflow publishes only after all required CI jobs, VM integration, all native package -jobs, and every manifest target's VM evidence pass. It verifies each package digest against its VM evidence, then assembles packages, -writes `dependency-inventory.json`, creates `checksums.txt`, signs that checksum -manifest with keyless Sigstore signing, and emits GitHub build provenance. The -workflow uses the repository's ephemeral `GITHUB_TOKEN`; no package registry, -VM host, signing key, password, or deployment credential is stored in this -repository. +the target remains pending or fails and the release stays blocked. + +The `release.yml` workflow runs CI, builds every manifest target with +`native-packages.yml` on GitHub-hosted runners, assembles one package per +target, and writes `checksums.txt`. It publishes a GitHub release for any +`v*` tag; manual dispatch builds and validates without publishing. The tag +is the release type: a hyphenated suffix (`v0.3.0-beta1`, `v0.3.0-alpha2`, +`v0.3.0-rc1`) publishes as a prerelease, a clean version (`v1.2.0`) +publishes as a stable release. Malformed tags fail fast before any packages +build. There is no VM evidence gate, no signing, and no provenance +attestation; validation of installed systems happens separately through the +lifecycle harness inside a disposable VM you provide. Run the Arch lifecycle harness inside a disposable Arch VM when both package versions are available: From db6d58e431357f9f145f92bfaeef8b0832f6f5c2 Mon Sep 17 00:00:00 2001 From: Krishna Santosh <75202541+krishna-santosh@users.noreply.github.com> Date: Sun, 20 Sep 2026 10:28:30 +0530 Subject: [PATCH 7/7] feat(release): replace beta workflow with tag-based release Replace beta-release.yml (v*-beta* with ci, integration, native-packages, vm-validation and cosign evidence gate) with release.yml triggered on v* tags and manual dispatch. The new workflow validates the tag, runs required CI, builds all native packages and publishes a GitHub release with checksums, using the hyphen suffix to mark prereleases. --- .github/workflows/beta-release.yml | 131 ----------------------------- .github/workflows/release.yml | 94 +++++++++++++++++++++ 2 files changed, 94 insertions(+), 131 deletions(-) delete mode 100644 .github/workflows/beta-release.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml deleted file mode 100644 index 75a7273..0000000 --- a/.github/workflows/beta-release.yml +++ /dev/null @@ -1,131 +0,0 @@ -name: Beta release - -on: - push: - tags: - - "v*-beta*" - workflow_dispatch: - -permissions: - contents: read - -jobs: - ci: - name: Required CI gates - uses: ./.github/workflows/ci.yml - permissions: - contents: read - - integration: - name: Required VM integration - needs: ci - uses: ./.github/workflows/integration.yml - permissions: - contents: read - - native-packages: - name: Native package matrix - needs: ci - uses: ./.github/workflows/native-packages.yml - permissions: - contents: read - - lifecycle-validation: - name: Representative lifecycle validation - needs: ci - uses: ./.github/workflows/packaging-lifecycle.yml - permissions: - contents: read - - vm-validation: - name: Disposable VM validation matrix - needs: native-packages - uses: ./.github/workflows/vm-validation.yml - permissions: - contents: read - actions: read - - evidence-gate: - name: Verify all VM evidence - needs: [ci, integration, native-packages, vm-validation, lifecycle-validation] - runs-on: ubuntu-24.04 - permissions: - contents: read - actions: read - id-token: write - attestations: write - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version-file: apps/backend/go.mod - cache: true - cache-dependency-path: | - apps/backend/go.sum - go.work.sum - - uses: oven-sh/setup-bun@v2 - with: - bun-version: 1.4.0 - - uses: actions/download-artifact@v4 - with: - pattern: package-* - path: incoming - merge-multiple: true - - uses: actions/download-artifact@v4 - with: - pattern: vm-evidence-* - path: incoming - merge-multiple: true - - name: Fail closed unless every manifest target passed in a VM - run: TAKO_VM_COMMIT="$GITHUB_SHA" ./tools/release-gate incoming incoming - - name: Assemble packages from the manifest - run: bun tools/release-assemble incoming release - - name: Record dependency inventory - run: bun tools/release-dependencies release/dependency-inventory.json - - name: Generate release checksums - run: | - set -euo pipefail - checksums_tmp=$(mktemp) - trap 'rm -f "$checksums_tmp"' EXIT - cd release - find . -maxdepth 1 -type f ! -name checksums.txt -print0 \ - | sort -z \ - | xargs -0 sha256sum > "$checksums_tmp" - mv "$checksums_tmp" checksums.txt - - uses: sigstore/cosign-installer@v3 - - name: Sign the release checksum manifest - run: | - cosign sign-blob --yes \ - --output-signature release/checksums.txt.sig \ - --output-certificate release/checksums.txt.pem \ - release/checksums.txt - - name: Attest release provenance - uses: actions/attest-build-provenance@v2 - with: - subject-path: release/* - - uses: actions/upload-artifact@v4 - with: - name: beta-release-inputs - path: release/ - if-no-files-found: error - retention-days: 30 - - publish: - name: Publish beta release - needs: evidence-gate - if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref_name, '-beta') - runs-on: ubuntu-24.04 - permissions: - contents: write - steps: - - uses: actions/download-artifact@v4 - with: - name: beta-release-inputs - path: release - - name: Publish only after CI, native builds, and every VM evidence gate passed - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.ref_name }} - prerelease: true - generate_release_notes: true - files: release/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7fc6e13 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,94 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + version: + name: Resolve version + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + prerelease: ${{ steps.resolve.outputs.prerelease }} + steps: + - name: Validate tag and resolve release type + id: resolve + run: | + set -euo pipefail + if [ "${{ github.event_name }}" != push ]; then + echo "prerelease=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + tag="${{ github.ref_name }}" + if ! printf '%s' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; then + echo "tag $tag does not match vMAJOR.MINOR.PATCH[-suffix]" >&2 + exit 1 + fi + if printf '%s' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+-'; then + echo "prerelease=true" >> "$GITHUB_OUTPUT" + else + echo "prerelease=false" >> "$GITHUB_OUTPUT" + fi + + ci: + name: Required CI + uses: ./.github/workflows/ci.yml + + native-packages: + name: Native packages + needs: ci + uses: ./.github/workflows/native-packages.yml + + publish: + name: Assemble and publish + needs: [native-packages, version] + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.4.0 + - uses: actions/download-artifact@v4 + with: + pattern: package-* + path: incoming + - name: Assemble every manifest package + run: bun tools/release-assemble incoming release + - name: Generate release checksums + working-directory: release + run: | + set -euo pipefail + checksums_tmp=$(mktemp) + trap 'rm -f "$checksums_tmp"' EXIT + find . -maxdepth 1 -type f ! -name checksums.txt -print0 \ + | sort -z \ + | xargs -0 sha256sum > "$checksums_tmp" + mv "$checksums_tmp" checksums.txt + - uses: actions/upload-artifact@v4 + with: + name: release-packages + path: release/ + if-no-files-found: error + retention-days: 30 + - name: Publish release + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + prerelease: ${{ needs.version.outputs.prerelease == 'true' }} + generate_release_notes: true + fail_on_unmatched_files: true + files: release/*