diff --git a/.enactr/README.md b/.enactr/README.md new file mode 100644 index 0000000..12d3ee3 --- /dev/null +++ b/.enactr/README.md @@ -0,0 +1,73 @@ +# Enactr flows + +Flywheel's Enactr CI and release automation lives in `flows/`. These definitions +coexist with the GitHub Actions workflows; switching off the GitHub workflows is +a separate cutover decision. + +## Git worktrees + +Every command action receives its own repository worktree. Actions without a +`git` block use Enactr's automatic checkout: the connected Flywheel repository +is cloned into `/workspace/src`, the run's exact `ENACTR_COMMIT` is checked out +detached, and nothing is published afterward. The `ci`, `main`, release-check, +image-build, manifest, and GitHub-release actions all use this read-only mode. + +Worktrees are isolated by action, including actions in the same group. A file +created by one action is not visible to another unless it is transferred +through Storage, a pushed Git ref, or another external artifact service. + +Only actions that need branch behavior or publication declare `git`: + +| Action | Worktree | After a successful command | +| --- | --- | --- | +| Release `helm` | `gh-pages` branch | Commit the chart archive and index, then push | + +The Helm action can own only its `gh-pages` worktree, so it downloads the source +archive addressed by `ENACTR_COMMIT` before packaging the chart. Native Git +credentials come from the connected repository's short-lived installation +token and are redacted by the runtime. `GH_TOKEN` remains separate because +GitHub release creation is a provider API operation outside the native Git +module. + +The flows expect these tenant secrets: + +| Secret | Used for | +| --- | --- | +| `CODECOV_TOKEN` | Uploading Rust coverage | +| `DOCKERHUB_USERNAME` | Authenticating to Docker Hub | +| `DOCKERHUB_TOKEN` | Pushing images and manifests to Docker Hub | +| `GH_TOKEN` | Creating GitHub releases | + +The `GH_TOKEN` credential needs write access to repository contents so GitHub +can create the release tag. Native Git actions use the connected repository's +short-lived installation token instead. Docker image builds run natively and +concurrently on the hosted `build-amd64` and `build-arm64` queues. Each build +pushes an architecture tag; the dependent manifest action combines those +images into the version tag and `latest`. + +Version bumps remain owned by `.github/workflows/bump.yaml`. Native Git +configuration is literal, so an Enactr bump flow cannot yet create a fresh +version-named branch safely; reusing a branch can start a later bump from stale +or abandoned release state. + +The Enactr `release` flow is manual-only while the GitHub and Enactr +implementations coexist. Before doing any release work, it verifies that an +existing tag points to the run commit and fails closed when the GitHub release +already exists. The push trigger should be enabled only in the cutover that +disables the GitHub release publisher. + +The release publishes the Helm index from a native `gh-pages` worktree; the +action packages the exact run commit from a GitHub source archive so the +worktree remains single-purpose. + +The original CI topology is represented by separate `ci` and `main` flows. +Format and Clippy fan out first; Test and the release build then run in +parallel, while Coverage waits specifically for Test. + +Two GitHub Actions behaviors do not yet have direct Enactr equivalents in these +flows: + +- CI does not cancel an older run for the same pull-request ref; Enactr + concurrency is flow-wide rather than grouped by ref. +- The Rust build cache is not persisted because no durable Enactr storage + backend has been selected for Cargo state. diff --git a/.enactr/flows/ci.yaml b/.enactr/flows/ci.yaml new file mode 100644 index 0000000..f933a70 --- /dev/null +++ b/.enactr/flows/ci.yaml @@ -0,0 +1,88 @@ +name: ci + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + workflow_dispatch: {} + +concurrency: 10 + +env: + CARGO_TERM_COLOR: always + +agents: + queue: default + +actions: + - group: Static checks + actions: + - label: Format + key: fmt + image: rust:1.94-bookworm + command: | + set -eu + rustup component add rustfmt + cargo fmt --check + + - label: Clippy + key: clippy + image: rust:1.94-bookworm + timeout: 30m + command: | + set -eu + apt-get update + apt-get install --yes --no-install-recommends libclang-dev + rm -rf /var/lib/apt/lists/* + rustup component add clippy + cargo clippy --all-targets --all-features -- -D warnings + + - group: Test, coverage, and build + actions: + - label: Test + key: test + image: rust:1.94-bookworm + timeout: 40m + command: | + set -eu + apt-get update + apt-get install --yes --no-install-recommends libclang-dev + rm -rf /var/lib/apt/lists/* + cargo test --all-targets + + - label: Coverage + key: coverage + image: rust:1.94-bookworm + depends_on: [test] + timeout: 60m + secrets: + CODECOV_TOKEN: enactr://CODECOV_TOKEN + command: | + set -eu + + apt-get update + apt-get install --yes --no-install-recommends curl libclang-dev + rm -rf /var/lib/apt/lists/* + rustup component add llvm-tools-preview + cargo install cargo-llvm-cov --locked + cargo llvm-cov --all-targets --lcov --output-path lcov.info + + curl -Os https://cli.codecov.io/latest/linux/codecov + chmod +x codecov + ./codecov --verbose upload-process \ + --disable-search \ + --fail-on-error \ + --token "$CODECOV_TOKEN" \ + --name "enactr-${ENACTR_RUN_ID}" \ + --file lcov.info + + - label: Release build + key: build + image: rust:1.94-bookworm + timeout: 40m + command: | + set -eu + apt-get update + apt-get install --yes --no-install-recommends libclang-dev + rm -rf /var/lib/apt/lists/* + cargo build --release diff --git a/.enactr/flows/main.yaml b/.enactr/flows/main.yaml new file mode 100644 index 0000000..c001eed --- /dev/null +++ b/.enactr/flows/main.yaml @@ -0,0 +1,86 @@ +name: main + +on: + push: + branches: [main] + +concurrency: 1 + +env: + CARGO_TERM_COLOR: always + +agents: + queue: default + +actions: + - group: Static checks + actions: + - label: Format + key: fmt + image: rust:1.94-bookworm + command: | + set -eu + rustup component add rustfmt + cargo fmt --check + + - label: Clippy + key: clippy + image: rust:1.94-bookworm + timeout: 30m + command: | + set -eu + apt-get update + apt-get install --yes --no-install-recommends libclang-dev + rm -rf /var/lib/apt/lists/* + rustup component add clippy + cargo clippy --all-targets --all-features -- -D warnings + + - group: Test, coverage, and build + actions: + - label: Test + key: test + image: rust:1.94-bookworm + timeout: 40m + command: | + set -eu + apt-get update + apt-get install --yes --no-install-recommends libclang-dev + rm -rf /var/lib/apt/lists/* + cargo test --all-targets + + - label: Coverage + key: coverage + image: rust:1.94-bookworm + depends_on: [test] + timeout: 60m + secrets: + CODECOV_TOKEN: enactr://CODECOV_TOKEN + command: | + set -eu + + apt-get update + apt-get install --yes --no-install-recommends curl libclang-dev + rm -rf /var/lib/apt/lists/* + rustup component add llvm-tools-preview + cargo install cargo-llvm-cov --locked + cargo llvm-cov --all-targets --lcov --output-path lcov.info + + curl -Os https://cli.codecov.io/latest/linux/codecov + chmod +x codecov + ./codecov --verbose upload-process \ + --disable-search \ + --fail-on-error \ + --token "$CODECOV_TOKEN" \ + --name "enactr-${ENACTR_RUN_ID}" \ + --file lcov.info + + - label: Release build + key: build + image: rust:1.94-bookworm + timeout: 40m + command: | + set -eu + apt-get update + apt-get install --yes --no-install-recommends libclang-dev + rm -rf /var/lib/apt/lists/* + cargo build --release diff --git a/.enactr/flows/release.yaml b/.enactr/flows/release.yaml new file mode 100644 index 0000000..2cf6af7 --- /dev/null +++ b/.enactr/flows/release.yaml @@ -0,0 +1,226 @@ +name: release + +on: + workflow_dispatch: {} + +concurrency: 1 + +env: + CARGO_TERM_COLOR: always + IMAGE_REPOSITORY: docker.io/ctxsh/flywheel + +agents: + queue: default + +actions: + - label: Preflight release + key: resolve-tag + image: alpine:3.22 + secrets: + GH_TOKEN: enactr://GH_TOKEN + command: | + set -eu + + apk add --no-cache github-cli + version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1) + tag="v${version}" + enactr metadata set tag "$tag" + + if tag_type=$(gh api "repos/ctxswitch/flywheel/git/ref/tags/${tag}" --jq '.object.type' 2>/dev/null); then + tag_sha=$(gh api "repos/ctxswitch/flywheel/git/ref/tags/${tag}" --jq '.object.sha') + if [ "$tag_type" = "tag" ]; then + tag_sha=$(gh api "repos/ctxswitch/flywheel/git/tags/${tag_sha}" --jq '.object.sha') + fi + if [ "$tag_sha" != "$ENACTR_COMMIT" ]; then + echo "Release tag $tag already points to $tag_sha, not $ENACTR_COMMIT." >&2 + exit 1 + fi + fi + + if gh release view "$tag" --repo ctxswitch/flywheel >/dev/null 2>&1; then + echo "GitHub release $tag already exists; refusing to republish it." >&2 + exit 1 + fi + echo "Release $tag is clear to publish from $ENACTR_COMMIT." + + - label: Release checks + key: release-checks + image: rust:1.94-bookworm + timeout: 60m + command: | + set -eu + + apt-get update + apt-get install --yes --no-install-recommends libclang-dev + rm -rf /var/lib/apt/lists/* + rustup component add rustfmt clippy + cargo fmt --check + cargo clippy --all-targets --all-features -- -D warnings + cargo test --all-targets + + - group: Native image builds + actions: + - label: Build linux/amd64 image + key: image-amd64 + image: quay.io/buildah/stable:v1.43 + agents: + queue: build-amd64 + timeout: 90m + env: + STORAGE_DRIVER: vfs + BUILDAH_ISOLATION: chroot + secrets: + DOCKERHUB_USERNAME: enactr://DOCKERHUB_USERNAME + DOCKERHUB_TOKEN: enactr://DOCKERHUB_TOKEN + command: | + set -eu + + test "$(uname -m)" = "x86_64" + tag=$(enactr metadata get tag) + destination="${IMAGE_REPOSITORY}:${tag}-amd64" + + printf '%s' "$DOCKERHUB_TOKEN" | + buildah login --username "$DOCKERHUB_USERNAME" --password-stdin docker.io + buildah bud \ + --arch amd64 \ + --format docker \ + --isolation chroot \ + --tag "$destination" \ + . + buildah push "$destination" "docker://$destination" + + - label: Build linux/arm64 image + key: image-arm64 + image: quay.io/buildah/stable:v1.43 + agents: + queue: build-arm64 + timeout: 90m + env: + STORAGE_DRIVER: vfs + BUILDAH_ISOLATION: chroot + secrets: + DOCKERHUB_USERNAME: enactr://DOCKERHUB_USERNAME + DOCKERHUB_TOKEN: enactr://DOCKERHUB_TOKEN + command: | + set -eu + + test "$(uname -m)" = "aarch64" + tag=$(enactr metadata get tag) + destination="${IMAGE_REPOSITORY}:${tag}-arm64" + + printf '%s' "$DOCKERHUB_TOKEN" | + buildah login --username "$DOCKERHUB_USERNAME" --password-stdin docker.io + buildah bud \ + --arch arm64 \ + --format docker \ + --isolation chroot \ + --tag "$destination" \ + . + buildah push "$destination" "docker://$destination" + + - label: Publish multi-architecture manifests + key: image-manifest + image: quay.io/buildah/stable:v1.43 + depends_on: [image-amd64, image-arm64] + timeout: 20m + env: + STORAGE_DRIVER: vfs + secrets: + DOCKERHUB_USERNAME: enactr://DOCKERHUB_USERNAME + DOCKERHUB_TOKEN: enactr://DOCKERHUB_TOKEN + command: | + set -eu + + tag=$(enactr metadata get tag) + manifest="flywheel-${ENACTR_RUN_ID}" + + printf '%s' "$DOCKERHUB_TOKEN" | + buildah login --username "$DOCKERHUB_USERNAME" --password-stdin docker.io + buildah manifest create "$manifest" + buildah manifest add \ + --arch amd64 \ + "$manifest" \ + "docker://${IMAGE_REPOSITORY}:${tag}-amd64" + buildah manifest add \ + --arch arm64 \ + "$manifest" \ + "docker://${IMAGE_REPOSITORY}:${tag}-arm64" + buildah manifest push --all "$manifest" "docker://${IMAGE_REPOSITORY}:${tag}" + buildah manifest push --all "$manifest" "docker://${IMAGE_REPOSITORY}:latest" + + - label: Publish Helm chart + key: helm + image: alpine/helm:3.20.2 + depends_on: [image-manifest] + timeout: 20m + git: + ref: gh-pages + fetch_depth: 0 + clean: true + commit: + message: "chore(release): publish chart" + author_name: github-actions[bot] + author_email: github-actions[bot]@users.noreply.github.com + push: + atomic: true + command: | + set -eu + + source_dir=$(mktemp -d) + trap 'rm -rf "$source_dir"' EXIT + wget -qO- \ + "https://github.com/ctxswitch/flywheel/archive/${ENACTR_COMMIT}.tar.gz" | + tar -xz --strip-components=1 -C "$source_dir" + + helm lint "$source_dir/charts/flywheel" --strict + helm package "$source_dir/charts/flywheel" --destination "$source_dir" + + cp "$source_dir"/*.tgz . + touch .nojekyll + if [ -f index.yaml ]; then + helm repo index . \ + --url https://ctxswitch.github.io/flywheel \ + --merge index.yaml + else + helm repo index . --url https://ctxswitch.github.io/flywheel + fi + + - label: Publish GitHub release + key: github-release + image: alpine:3.22 + depends_on: [image-manifest, helm] + timeout: 20m + secrets: + GH_TOKEN: enactr://GH_TOKEN + command: | + set -eu + + apk add --no-cache github-cli + tag=$(enactr metadata get tag) + chart_version=$(sed -n 's/^version: *//p' charts/flywheel/Chart.yaml | head -1) + + notes=$(cat <> "$GITHUB_OUTPUT" else echo "release=true" >> "$GITHUB_OUTPUT" + echo "source_ref=$tag" >> "$GITHUB_OUTPUT" fi else - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag "$tag" - git push origin "$tag" echo "release=true" >> "$GITHUB_OUTPUT" + echo "source_ref=${{ github.sha }}" >> "$GITHUB_OUTPUT" fi echo "tag=$tag" >> "$GITHUB_OUTPUT" @@ -60,7 +59,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.resolve-tag.outputs.tag }} + ref: ${{ needs.resolve-tag.outputs.source_ref }} - name: Install native build dependencies run: sudo apt-get update && sudo apt-get install --yes --no-install-recommends libclang-dev - uses: dtolnay/rust-toolchain@stable @@ -79,7 +78,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.resolve-tag.outputs.tag }} + ref: ${{ needs.resolve-tag.outputs.source_ref }} - uses: docker/login-action@v3 with: @@ -108,7 +107,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.resolve-tag.outputs.tag }} + ref: ${{ needs.resolve-tag.outputs.source_ref }} - name: Install Helm uses: azure/setup-helm@v4 @@ -154,7 +153,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.resolve-tag.outputs.tag }} + ref: ${{ needs.resolve-tag.outputs.source_ref }} fetch-depth: 0 - name: Read chart version @@ -166,11 +165,13 @@ jobs: - name: Generate changelog id: changelog run: | - prev_tag=$(git tag --sort=-v:refname | grep '^v' | sed -n '2p') + tag='${{ needs.resolve-tag.outputs.tag }}' + source_ref='${{ needs.resolve-tag.outputs.source_ref }}' + prev_tag=$(git tag --sort=-v:refname | grep '^v' | grep -Fvx "$tag" | sed -n '1p') if [ -z "$prev_tag" ]; then - log=$(git log --pretty=format:"- %s (%h)" '${{ needs.resolve-tag.outputs.tag }}') + log=$(git log --pretty=format:"- %s (%h)" "$source_ref") else - log=$(git log --pretty=format:"- %s (%h)" "${prev_tag}..${{ needs.resolve-tag.outputs.tag }}") + log=$(git log --pretty=format:"- %s (%h)" "${prev_tag}..${source_ref}") fi { @@ -183,6 +184,7 @@ jobs: uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.resolve-tag.outputs.tag }} + target_commitish: ${{ needs.resolve-tag.outputs.source_ref }} generate_release_notes: false body: | ## Docker Image