From ee8df43234ca39fb1241689898171adddd17e7f5 Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Wed, 18 Mar 2026 11:10:02 +0200 Subject: [PATCH 1/7] release: extract CLI build into shared build_cli action Extract the CLI build into a reusable composite action at .github/actions/build_cli that takes a system input (e.g. x86_64-linux, aarch64-darwin). The binary is copied to workspace/contrast-${SYSTEM} so each platform gets a distinct artifact name. Update artifact paths and update-contrast-releases.sh to match. Signed-off-by: Spyros Seimenis --- .github/actions/build_cli/action.yml | 20 ++++++++++++++++++++ .github/actions/release_artifacts/action.yml | 11 ++++------- packages/update-contrast-releases.sh | 2 +- 3 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 .github/actions/build_cli/action.yml diff --git a/.github/actions/build_cli/action.yml b/.github/actions/build_cli/action.yml new file mode 100644 index 00000000000..5743152707a --- /dev/null +++ b/.github/actions/build_cli/action.yml @@ -0,0 +1,20 @@ +name: build CLI +description: Build the Contrast CLI release binary for a given system + +inputs: + system: + description: Nix system (e.g. x86_64-linux, aarch64-darwin) + required: true + +runs: + using: "composite" + steps: + - name: Build CLI + shell: bash + env: + SET: base + SYSTEM: ${{ inputs.system }} + run: | + mkdir -p workspace + nix build -L ".#${SET}.contrast.cli-release" + cp result/bin/contrast "workspace/contrast-${SYSTEM}" diff --git a/.github/actions/release_artifacts/action.yml b/.github/actions/release_artifacts/action.yml index e2b5c481840..ffe2a7e9ac2 100644 --- a/.github/actions/release_artifacts/action.yml +++ b/.github/actions/release_artifacts/action.yml @@ -113,7 +113,7 @@ runs: run: | cat << 'EOF' | tee -a "${GITHUB_OUTPUT}" paths< workspace/node-installer-target-config-k3s.yml - - name: Build CLI - shell: bash - env: - SET: base - run: | - nix build -L ".#${SET}.contrast.cli-release" --out-link workspace/contrast-cli + - uses: ./.github/actions/build_cli + with: + system: x86_64-linux - name: AWS login (IAM role) uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 with: diff --git a/packages/update-contrast-releases.sh b/packages/update-contrast-releases.sh index e3dfdb300df..54ccecc9332 100755 --- a/packages/update-contrast-releases.sh +++ b/packages/update-contrast-releases.sh @@ -31,7 +31,7 @@ target_configs=( # declare an associative array that pairs the field name # in ./packages/versions.json with the path to the file declare -A fields -fields["contrast"]="./workspace/contrast-cli/bin/contrast" +fields["contrast"]="./workspace/contrast-x86_64-linux" fields["coordinator.yml"]="./workspace/coordinator.yml" fields["runtime.yml"]="./workspace/runtime.yml" fields["emojivoto-demo.zip"]="./workspace/emojivoto-demo.zip" From 7477d11c9190a369020b94dfc5b71123da92eb3a Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Thu, 16 Apr 2026 13:44:52 +0300 Subject: [PATCH 2/7] release: make S3 pre-release paths deterministic per workflow attempt Replace the unix timestamp (date +%s) in pre-release S3 paths with GitHub Actions run metadata. Use github.run_id so multiple jobs in the same workflow run can independently compute the same S3 prefix without inter-job output plumbing. Include github.run_attempt so reruns publish to a different URL instead of silently overwriting artifacts that may already have been shared with testers. This keeps x86_64-linux and aarch64-darwin uploads coordinated within one workflow attempt while preserving immutable customer-facing links across reruns. Signed-off-by: Spyros Seimenis --- .github/actions/release_artifacts/action.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/actions/release_artifacts/action.yml b/.github/actions/release_artifacts/action.yml index ffe2a7e9ac2..a2a480e43d3 100644 --- a/.github/actions/release_artifacts/action.yml +++ b/.github/actions/release_artifacts/action.yml @@ -192,19 +192,18 @@ runs: id: upload-artifacts shell: bash env: - S3_BUCKET_PATH: ${{ inputs.s3-bucket-path }} + S3_BUCKET_PATH: ${{ inputs.s3-bucket-path }}/${{ github.run_id }}/${{ github.run_attempt }} FILES: ${{ steps.artifact-paths.outputs.paths }} run: | set -u shopt -s nullglob - unix=$(date +%s) readarray -t patterns <<< "$FILES" for pattern in "${patterns[@]}"; do matches=($pattern) for file in "${matches[@]}"; do - aws s3 cp "$file" "s3://contrast-public/${S3_BUCKET_PATH}/$unix/" + aws s3 cp "$file" "s3://contrast-public/${S3_BUCKET_PATH}/" done done - url="https://contrast-public.s3.eu-central-1.amazonaws.com/${S3_BUCKET_PATH}/$unix/" + url="https://contrast-public.s3.eu-central-1.amazonaws.com/${S3_BUCKET_PATH}/" echo "Artifacts available at $url" | tee -a "$GITHUB_STEP_SUMMARY" echo "url=$url" | tee -a "$GITHUB_OUTPUT" From 1e38d6757269b8009afc8338234ef459f00e6d25 Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Thu, 16 Apr 2026 13:45:04 +0300 Subject: [PATCH 3/7] release: extract S3 upload into shared s3_upload action Move the AWS IAM role login and S3 upload logic into a reusable composite action at .github/actions/s3_upload. Signed-off-by: Spyros Seimenis --- .github/actions/release_artifacts/action.yml | 29 +++----------- .github/actions/s3_upload/action.yml | 42 ++++++++++++++++++++ 2 files changed, 47 insertions(+), 24 deletions(-) create mode 100644 .github/actions/s3_upload/action.yml diff --git a/.github/actions/release_artifacts/action.yml b/.github/actions/release_artifacts/action.yml index a2a480e43d3..0a06a77816d 100644 --- a/.github/actions/release_artifacts/action.yml +++ b/.github/actions/release_artifacts/action.yml @@ -182,28 +182,9 @@ runs: - uses: ./.github/actions/build_cli with: system: x86_64-linux - - name: AWS login (IAM role) - uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 - with: - role-to-assume: arn:aws:iam::795746500882:role/ContrastPublicBucketRW - aws-region: eu-central-1 - - name: Upload pre-release artifacts to S3 bucket contrast-public + - id: upload-artifacts + uses: ./.github/actions/s3_upload if: ${{ inputs.s3-bucket-path != '' }} - id: upload-artifacts - shell: bash - env: - S3_BUCKET_PATH: ${{ inputs.s3-bucket-path }}/${{ github.run_id }}/${{ github.run_attempt }} - FILES: ${{ steps.artifact-paths.outputs.paths }} - run: | - set -u - shopt -s nullglob - readarray -t patterns <<< "$FILES" - for pattern in "${patterns[@]}"; do - matches=($pattern) - for file in "${matches[@]}"; do - aws s3 cp "$file" "s3://contrast-public/${S3_BUCKET_PATH}/" - done - done - url="https://contrast-public.s3.eu-central-1.amazonaws.com/${S3_BUCKET_PATH}/" - echo "Artifacts available at $url" | tee -a "$GITHUB_STEP_SUMMARY" - echo "url=$url" | tee -a "$GITHUB_OUTPUT" + with: + files: ${{ steps.artifact-paths.outputs.paths }} + s3-bucket-path: ${{ inputs.s3-bucket-path }}/${{ github.run_id }}/${{ github.run_attempt }} diff --git a/.github/actions/s3_upload/action.yml b/.github/actions/s3_upload/action.yml new file mode 100644 index 00000000000..976be12346a --- /dev/null +++ b/.github/actions/s3_upload/action.yml @@ -0,0 +1,42 @@ +name: upload to S3 +description: Upload files to the contrast-public S3 bucket (assumes role via OIDC) + +inputs: + files: + description: Newline-separated list of files or glob patterns to upload + required: true + s3-bucket-path: + description: S3 key prefix (under the contrast-public bucket) + required: true + +outputs: + url: + description: Public URL of the uploaded artifacts + value: ${{ steps.upload-artifacts.outputs.url }} + +runs: + using: "composite" + steps: + - name: AWS login (IAM role) + uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 + with: + role-to-assume: arn:aws:iam::795746500882:role/ContrastPublicBucketRW + aws-region: eu-central-1 + - name: Upload pre-release artifacts to S3 bucket contrast-public + id: upload-artifacts + shell: bash + env: + S3_BUCKET_PATH: ${{ inputs.s3-bucket-path }} + FILES: ${{ inputs.files }} + run: | + shopt -s nullglob + readarray -t patterns <<< "$FILES" + for pattern in "${patterns[@]}"; do + matches=($pattern) + for file in "${matches[@]}"; do + aws s3 cp "$file" "s3://contrast-public/${S3_BUCKET_PATH}/" + done + done + url="https://contrast-public.s3.eu-central-1.amazonaws.com/${S3_BUCKET_PATH}/" + echo "Artifacts available at $url" | tee -a "$GITHUB_STEP_SUMMARY" + echo "url=$url" | tee -a "$GITHUB_OUTPUT" From 58c4892ff617b2c8370f62aa030de937b92b8327 Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Thu, 19 Mar 2026 11:03:33 +0200 Subject: [PATCH 4/7] release: add aarch64-darwin CLI to release pipeline Add a release-aarch64-darwin job that builds the CLI on macos-latest, attaches the binary to the draft GitHub release, and uploads it to the S3 pre-release bucket. The job runs after release-x86_64-linux to ensure cachix is warm with the required linux derivations and the draft release exists. Rename the release job to release-x86_64-linux for consistency. The publish job waits for both release jobs before marking the release as non-draft. The update-main job downloads the darwin artifact so that update-contrast-releases.sh can hash it into contrast-releases.json. Signed-off-by: Spyros Seimenis --- .github/workflows/release.yml | 64 +++++++++++++++++++++++++--- packages/update-contrast-releases.sh | 1 + 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2bf92223803..988dd192743 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -121,7 +121,7 @@ jobs: update-main: name: Update main branch runs-on: ubuntu-24.04 - needs: [process-inputs, release] + needs: [process-inputs, release-x86_64-linux, release-aarch64-darwin] permissions: contents: write env: @@ -182,6 +182,11 @@ jobs: with: name: contrast-release-artifacts path: ./contrast-main + - name: Download darwin CLI artifact + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: contrast-cli-aarch64-darwin + path: ./contrast-main/workspace - name: Update contrast-releases.json with new release working-directory: contrast-main run: nix run ".#${SET}.scripts.update-contrast-releases" @@ -217,7 +222,56 @@ jobs: token: ${{ secrets.NUNKI_CI_COMMIT_PUSH_PR }} path: ./contrast-main - release: + release-aarch64-darwin: + name: Build aarch64-darwin CLI + runs-on: macos-latest + needs: [process-inputs, release-x86_64-linux] + permissions: + contents: write + id-token: write + env: + VERSION: ${{ inputs.version }} + SET: base + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ needs.process-inputs.outputs.WORKING_BRANCH }} + persist-credentials: false + - uses: ./.github/actions/setup_nix + with: + githubToken: ${{ secrets.GITHUB_TOKEN }} + cachixToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + - name: Bump flake version to release version + uses: ./.github/actions/bump_version + with: + version: ${{ needs.process-inputs.outputs.WITHOUT_V }} + commit: false + - uses: ./.github/actions/build_cli + with: + system: aarch64-darwin + - name: Upload artifact + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: contrast-cli-aarch64-darwin + path: workspace/contrast-aarch64-darwin + - name: Remove existing darwin asset from draft release + env: + GH_TOKEN: ${{ github.token }} + run: | + if gh release view "${VERSION}" --repo "${{ github.repository }}" --json assets --jq '.assets[].name' | grep -Fxq 'contrast-aarch64-darwin'; then + gh release delete-asset "${VERSION}" contrast-aarch64-darwin --repo "${{ github.repository }}" --yes + fi + - name: Attach to release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release upload "${VERSION}" workspace/contrast-aarch64-darwin --repo "${{ github.repository }}" + - uses: ./.github/actions/s3_upload + with: + files: workspace/contrast-aarch64-darwin + s3-bucket-path: pre-releases/${{ inputs.version }}/${{ github.run_id }}/${{ github.run_attempt }} + + release-x86_64-linux: name: Build and push artifacts, create release runs-on: ubuntu-24.04 needs: process-inputs @@ -315,7 +369,7 @@ jobs: # Job needs content:write to see draft releases. contents: write packages: read - needs: [process-inputs, release, nightly] + needs: [process-inputs, release-x86_64-linux, nightly] env: VERSION: ${{ inputs.version }} SET: base @@ -387,7 +441,7 @@ jobs: nightly: name: e2e nightly - needs: release + needs: release-x86_64-linux uses: ./.github/workflows/e2e_nightly.yml secrets: GITHUB_TOKEN_IN: ${{ secrets.GITHUB_TOKEN }} @@ -401,7 +455,7 @@ jobs: publish: name: Publish release - needs: [process-inputs, release, test] + needs: [process-inputs, release-x86_64-linux, release-aarch64-darwin, test] runs-on: ubuntu-24.04 environment: release-publish permissions: diff --git a/packages/update-contrast-releases.sh b/packages/update-contrast-releases.sh index 54ccecc9332..c866c6f7b5b 100755 --- a/packages/update-contrast-releases.sh +++ b/packages/update-contrast-releases.sh @@ -32,6 +32,7 @@ target_configs=( # in ./packages/versions.json with the path to the file declare -A fields fields["contrast"]="./workspace/contrast-x86_64-linux" +fields["contrast-aarch64-darwin"]="./workspace/contrast-aarch64-darwin" fields["coordinator.yml"]="./workspace/coordinator.yml" fields["runtime.yml"]="./workspace/runtime.yml" fields["emojivoto-demo.zip"]="./workspace/emojivoto-demo.zip" From 3667d48630cf6278583c9bd3e5b7676bac2bbaa2 Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Tue, 24 Mar 2026 19:41:47 +0200 Subject: [PATCH 5/7] docs: add macOS install instructions for CLI Add a tabbed platform selector (Linux/macOS) to the CLI install page. The macOS tab downloads the aarch64-darwin binary and includes a note about the Gatekeeper quarantine workaround for browser downloads. Signed-off-by: Spyros Seimenis --- docs/docs/howto/install-cli.md | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/docs/docs/howto/install-cli.md b/docs/docs/howto/install-cli.md index 406590b2f07..63077adb31f 100644 --- a/docs/docs/howto/install-cli.md +++ b/docs/docs/howto/install-cli.md @@ -12,14 +12,33 @@ Required for deploying with Contrast. ## How-to -Download the Contrast CLI from the latest release: +Download the Contrast CLI from the latest release and install it in your PATH: + + + ```bash -curl --proto '=https' --tlsv1.2 -fLo contrast https://github.com/edgelesssys/contrast/releases/latest/download/contrast +curl --proto '=https' --tlsv1.2 -fLo contrast https://github.com/edgelesssys/contrast/releases/latest/download/contrast-x86_64-linux +sudo install contrast /usr/local/bin/contrast ``` -After that, install the Contrast CLI in your PATH, e.g.: + + ```bash +curl --proto '=https' --tlsv1.2 -fLo contrast https://github.com/edgelesssys/contrast/releases/latest/download/contrast-aarch64-darwin sudo install contrast /usr/local/bin/contrast ``` + +:::note +If you download the binary via a web browser instead of `curl`, macOS may show a warning +that the software can't be verified. Remove the quarantine attribute before installing the binary: + +```bash +sudo xattr -d com.apple.quarantine contrast +``` + +::: + + + From dd236095ac8d74100451544ac680dc79ff066c94 Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Fri, 17 Apr 2026 14:55:24 +0300 Subject: [PATCH 6/7] ci: add darwin CLI to pr release artifacts workflow Split the workflow into three jobs so the PR comment only posts once all artifacts are available in S3: 1. create-release-artifacts (ubuntu-24.04): builds containers and linux CLI, uploads to S3, exposes the S3 URL as a job output. 2. build-darwin-cli (macos-latest): needs job 1 (for Cachix to be populated), builds darwin CLI, uploads to the same S3 directory. 3. notify (ubuntu-24.04): needs both, posts the PR comment using the S3 URL from job 1. Signed-off-by: Spyros Seimenis --- .github/workflows/pr_release_artifacts.yml | 42 ++++++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr_release_artifacts.yml b/.github/workflows/pr_release_artifacts.yml index e9651c7acef..cc0aa659a98 100644 --- a/.github/workflows/pr_release_artifacts.yml +++ b/.github/workflows/pr_release_artifacts.yml @@ -17,11 +17,11 @@ jobs: if: github.event_name == 'workflow_dispatch' && github.event.inputs.cleanup == 'false' runs-on: ubuntu-24.04 permissions: - pull-requests: write - issues: write contents: read packages: write id-token: write + outputs: + s3-bucket-url: ${{ steps.create-artifacts.outputs.s3-bucket-url }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -51,6 +51,42 @@ jobs: version: ${{ steps.get-version.outputs.version }} container_registry: ghcr.io/edgelesssys s3-bucket-path: "pr-artifacts" + + build-darwin-cli: + name: Build aarch64-darwin CLI for PR + if: github.event_name == 'workflow_dispatch' && github.event.inputs.cleanup == 'false' + needs: [create-release-artifacts] + runs-on: macos-latest + permissions: + contents: read + id-token: write + env: + SET: base + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: ./.github/actions/setup_nix + with: + githubToken: ${{ secrets.GITHUB_TOKEN }} + cachixToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + - uses: ./.github/actions/build_cli + with: + system: aarch64-darwin + - uses: ./.github/actions/s3_upload + with: + files: workspace/contrast-aarch64-darwin + s3-bucket-path: pr-artifacts/${{ github.run_id }}/${{ github.run_attempt }} + + notify: + name: Create PR comment with artifact links + if: github.event_name == 'workflow_dispatch' && github.event.inputs.cleanup == 'false' + needs: [create-release-artifacts, build-darwin-cli] + runs-on: ubuntu-24.04 + permissions: + pull-requests: write + issues: write + steps: - name: Get PR number id: get-pr-number env: @@ -75,7 +111,7 @@ jobs: The pre-release artifacts for this commit are available at the following link: - ${{ steps.create-artifacts.outputs.s3-bucket-url }} + ${{ needs.create-release-artifacts.outputs.s3-bucket-url }} Created by @${{ github.actor }} in [pr_release_artifacts workflow]( https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}?pr=${{ steps.get-pr-number.outputs.pr_number }} From cdef68b87aad1a6b51dd46f5b7dd1b7413c03ce6 Mon Sep 17 00:00:00 2001 From: Spyros Seimenis Date: Fri, 17 Apr 2026 16:21:32 +0300 Subject: [PATCH 7/7] ci: fix s3_upload action on macOS readarray is a bash 4+ builtin not available on macOS which ships bash 3.2. Replace with a while-read loop which is portable across both bash versions. Signed-off-by: Spyros Seimenis --- .github/actions/s3_upload/action.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/actions/s3_upload/action.yml b/.github/actions/s3_upload/action.yml index 976be12346a..6f4e87554fa 100644 --- a/.github/actions/s3_upload/action.yml +++ b/.github/actions/s3_upload/action.yml @@ -30,13 +30,11 @@ runs: FILES: ${{ inputs.files }} run: | shopt -s nullglob - readarray -t patterns <<< "$FILES" - for pattern in "${patterns[@]}"; do - matches=($pattern) - for file in "${matches[@]}"; do + while IFS= read -r pattern; do + for file in $pattern; do aws s3 cp "$file" "s3://contrast-public/${S3_BUCKET_PATH}/" done - done + done <<< "$FILES" url="https://contrast-public.s3.eu-central-1.amazonaws.com/${S3_BUCKET_PATH}/" echo "Artifacts available at $url" | tee -a "$GITHUB_STEP_SUMMARY" echo "url=$url" | tee -a "$GITHUB_OUTPUT"