mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
651 lines
23 KiB
YAML
651 lines
23 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Tag to release, for example v0.1.0'
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
REGISTRY: ghcr.io
|
|
IMAGE_NAME: zyycn/codex-proxy-rs
|
|
|
|
jobs:
|
|
release-info:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
tag: ${{ steps.version.outputs.tag }}
|
|
version: ${{ steps.version.outputs.version }}
|
|
build_time: ${{ steps.version.outputs.build_time }}
|
|
git_sha: ${{ steps.version.outputs.git_sha }}
|
|
platform_matrix: ${{ steps.version.outputs.platform_matrix }}
|
|
docker_matrix: ${{ steps.version.outputs.docker_matrix }}
|
|
non_docker_matrix: ${{ steps.version.outputs.non_docker_matrix }}
|
|
docker_platforms: ${{ steps.version.outputs.docker_platforms }}
|
|
previous_tag: ${{ steps.version.outputs.previous_tag }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
|
|
fetch-depth: 0
|
|
|
|
- name: Read release metadata
|
|
id: version
|
|
shell: bash
|
|
env:
|
|
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
|
|
run: |
|
|
ruby <<'RUBY' >> "$GITHUB_OUTPUT"
|
|
require "json"
|
|
require "open3"
|
|
require "rubygems"
|
|
require "time"
|
|
require "yaml"
|
|
|
|
APP_NAME = "codex-proxy-rs"
|
|
SEMVER = /\A(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\z/
|
|
|
|
def fail(message)
|
|
warn(message)
|
|
exit(1)
|
|
end
|
|
|
|
def require_string(data, key, path)
|
|
value = data[key]
|
|
fail("#{path}: #{key} must be a non-empty string") unless value.is_a?(String) && !value.strip.empty?
|
|
value.strip
|
|
end
|
|
|
|
def version_sort_key(version)
|
|
Gem::Version.new(version.split("+", 2).first)
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
|
|
version_path = "release/version.yaml"
|
|
platforms_path = "release/platforms.yaml"
|
|
version_data = YAML.load_file(version_path)
|
|
platforms_data = YAML.load_file(platforms_path)
|
|
|
|
version = require_string(version_data, "version", version_path).delete_prefix("v")
|
|
tag_prefix = "v"
|
|
expected_tag = "#{tag_prefix}#{version}"
|
|
release_tag = ENV.fetch("RELEASE_TAG")
|
|
fail("Release tag #{release_tag} does not match #{version_path} tag #{expected_tag}") unless release_tag == expected_tag
|
|
fail("#{version_path}: version #{version} is not a valid semver") unless version.match?(SEMVER)
|
|
|
|
platforms = platforms_data["platforms"]
|
|
fail("#{platforms_path}: platforms must be a non-empty array") unless platforms.is_a?(Array) && !platforms.empty?
|
|
|
|
required_platform_keys = %w[os arch runner target binary archive]
|
|
seen_platforms = {}
|
|
docker_platform_jobs = []
|
|
non_docker_platform_jobs = []
|
|
docker_platforms = []
|
|
platforms.each_with_index do |platform, index|
|
|
fail("#{platforms_path}: platforms[#{index}] must be a table") unless platform.is_a?(Hash)
|
|
required_platform_keys.each do |key|
|
|
require_string(platform, key, "#{platforms_path}: platforms[#{index}]")
|
|
end
|
|
archive = platform["archive"]
|
|
fail("#{platforms_path}: platforms[#{index}].archive must be tar.gz or zip") unless ["tar.gz", "zip"].include?(archive)
|
|
platform_key = "#{platform["os"]}-#{platform["arch"]}"
|
|
fail("#{platforms_path}: duplicate platform #{platform_key}") if seen_platforms[platform_key]
|
|
seen_platforms[platform_key] = true
|
|
platform["artifact"] = "#{APP_NAME}-#{platform_key}"
|
|
if platform["docker"] == true
|
|
docker_platform = require_string(platform, "dockerPlatform", "#{platforms_path}: platforms[#{index}]")
|
|
platform["dockerPlatform"] = docker_platform
|
|
docker_platform_jobs << platform
|
|
docker_platforms << docker_platform
|
|
else
|
|
non_docker_platform_jobs << platform
|
|
end
|
|
end
|
|
|
|
fail("#{platforms_path}: at least one platform must set docker = true") if docker_platforms.empty?
|
|
|
|
git_sha, status = Open3.capture2("git", "rev-parse", "HEAD")
|
|
fail("failed to resolve git sha") unless status.success?
|
|
matrix = JSON.generate({ "include" => platforms })
|
|
docker_matrix = JSON.generate({ "include" => docker_platform_jobs })
|
|
non_docker_matrix = JSON.generate({ "include" => non_docker_platform_jobs })
|
|
current_sort_key = version_sort_key(version)
|
|
tags, status = Open3.capture2("git", "tag", "--list", "#{tag_prefix}*", "--merged", "HEAD")
|
|
fail("failed to list git tags") unless status.success?
|
|
previous_tag = tags
|
|
.lines
|
|
.map(&:strip)
|
|
.reject(&:empty?)
|
|
.reject { |tag| tag == release_tag }
|
|
.filter_map do |tag|
|
|
tag_version = tag.delete_prefix(tag_prefix)
|
|
next unless tag_version.match?(SEMVER)
|
|
sort_key = version_sort_key(tag_version)
|
|
next unless sort_key && current_sort_key && sort_key < current_sort_key
|
|
[sort_key, tag]
|
|
end
|
|
.max_by(&:first)
|
|
&.last
|
|
|
|
puts "tag=#{release_tag}"
|
|
puts "version=#{version}"
|
|
puts "build_time=#{Time.now.utc.iso8601}"
|
|
puts "git_sha=#{git_sha.strip}"
|
|
puts "platform_matrix=#{matrix}"
|
|
puts "docker_matrix=#{docker_matrix}"
|
|
puts "non_docker_matrix=#{non_docker_matrix}"
|
|
puts "docker_platforms=#{docker_platforms.join(",")}"
|
|
puts "previous_tag=#{previous_tag}"
|
|
RUBY
|
|
|
|
build-frontend:
|
|
needs: [release-info]
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.release-info.outputs.tag }}
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 11.7.0
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 24
|
|
cache: pnpm
|
|
cache-dependency-path: frontend/pnpm-lock.yaml
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
working-directory: frontend
|
|
|
|
- name: Build frontend
|
|
run: pnpm build
|
|
working-directory: frontend
|
|
|
|
- name: Upload frontend artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: frontend-dist
|
|
path: frontend/dist
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
build-docker-binary:
|
|
name: build-docker-binary (${{ matrix.os }}-${{ matrix.arch }})
|
|
needs: [release-info]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.release-info.outputs.docker_matrix) }}
|
|
runs-on: ${{ matrix.runner }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.release-info.outputs.tag }}
|
|
|
|
- name: Cache Cargo
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: |
|
|
~/.cargo/registry
|
|
~/.cargo/git
|
|
backend/target
|
|
key: cargo-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('backend/Cargo.lock') }}
|
|
restore-keys: |
|
|
cargo-${{ runner.os }}-${{ matrix.target }}-
|
|
|
|
- name: Build binary
|
|
shell: bash
|
|
run: |
|
|
mkdir -p "$HOME/.cargo" "../dist/binaries/${{ matrix.os }}-${{ matrix.arch }}"
|
|
docker run --rm \
|
|
--platform "${{ matrix.dockerPlatform }}" \
|
|
--user "$(id -u):$(id -g)" \
|
|
-e CARGO_HOME=/cargo \
|
|
-e HOME=/tmp \
|
|
-e CPR_VERSION="${CPR_VERSION}" \
|
|
-e CPR_GIT_SHA="${CPR_GIT_SHA}" \
|
|
-e CPR_BUILD_TIME="${CPR_BUILD_TIME}" \
|
|
-e CPR_BUILD_TYPE="${CPR_BUILD_TYPE}" \
|
|
-v "$HOME/.cargo:/cargo" \
|
|
-v "$PWD/..:/workspace" \
|
|
-w /workspace/backend \
|
|
rust:1.95-bookworm \
|
|
cargo build --release --locked --target "${{ matrix.target }}" --bin codex-proxy-rs
|
|
mkdir -p "../dist/binaries/${{ matrix.os }}-${{ matrix.arch }}"
|
|
cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "../dist/binaries/${{ matrix.os }}-${{ matrix.arch }}/${{ matrix.binary }}"
|
|
working-directory: backend
|
|
env:
|
|
CPR_VERSION: ${{ needs.release-info.outputs.version }}
|
|
CPR_GIT_SHA: ${{ needs.release-info.outputs.git_sha }}
|
|
CPR_BUILD_TIME: ${{ needs.release-info.outputs.build_time }}
|
|
CPR_BUILD_TYPE: release
|
|
|
|
- name: Upload binary artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: codex-proxy-rs-${{ matrix.os }}-${{ matrix.arch }}
|
|
path: dist/binaries/${{ matrix.os }}-${{ matrix.arch }}/
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
build-binary:
|
|
name: build-binary (${{ matrix.os }}-${{ matrix.arch }})
|
|
needs: [release-info]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.release-info.outputs.non_docker_matrix) }}
|
|
runs-on: ${{ matrix.runner }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.release-info.outputs.tag }}
|
|
|
|
- name: Cache Cargo
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: |
|
|
~/.cargo/registry
|
|
~/.cargo/git
|
|
backend/target
|
|
key: cargo-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('backend/Cargo.lock') }}
|
|
restore-keys: |
|
|
cargo-${{ runner.os }}-${{ matrix.target }}-
|
|
|
|
- name: Install Rust target
|
|
run: rustup target add ${{ matrix.target }}
|
|
working-directory: backend
|
|
|
|
- name: Build binary
|
|
shell: bash
|
|
run: |
|
|
cargo build --release --locked --target "${{ matrix.target }}" --bin codex-proxy-rs
|
|
mkdir -p "../dist/binaries/${{ matrix.os }}-${{ matrix.arch }}"
|
|
cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "../dist/binaries/${{ matrix.os }}-${{ matrix.arch }}/${{ matrix.binary }}"
|
|
working-directory: backend
|
|
env:
|
|
CPR_VERSION: ${{ needs.release-info.outputs.version }}
|
|
CPR_GIT_SHA: ${{ needs.release-info.outputs.git_sha }}
|
|
CPR_BUILD_TIME: ${{ needs.release-info.outputs.build_time }}
|
|
CPR_BUILD_TYPE: release
|
|
|
|
- name: Upload binary artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: codex-proxy-rs-${{ matrix.os }}-${{ matrix.arch }}
|
|
path: dist/binaries/${{ matrix.os }}-${{ matrix.arch }}/
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
package-assets:
|
|
needs: [release-info, build-frontend, build-docker-binary, build-binary]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
attestations: write
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.release-info.outputs.tag }}
|
|
|
|
- name: Download frontend artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: frontend-dist
|
|
path: dist/frontend
|
|
|
|
- name: Download binary artifacts
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: codex-proxy-rs-*
|
|
path: dist/binaries
|
|
|
|
- name: Package release assets
|
|
shell: bash
|
|
env:
|
|
VERSION: ${{ needs.release-info.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
ruby <<'RUBY' > release-platforms.json
|
|
require "json"
|
|
require "yaml"
|
|
|
|
print JSON.generate(YAML.load_file("release/platforms.yaml"))
|
|
RUBY
|
|
python3 - <<'PY'
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import shutil
|
|
import tarfile
|
|
import zipfile
|
|
|
|
APP_NAME = "codex-proxy-rs"
|
|
|
|
def sha256(path: pathlib.Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as file:
|
|
for chunk in iter(lambda: file.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
version = os.environ["VERSION"]
|
|
platforms = json.loads(pathlib.Path("release-platforms.json").read_text())["platforms"]
|
|
frontend_dir = pathlib.Path("dist/frontend")
|
|
binary_artifacts_dir = pathlib.Path("dist/binaries")
|
|
release_dir = pathlib.Path("dist/release")
|
|
package_dir = pathlib.Path("package")
|
|
|
|
shutil.rmtree(release_dir, ignore_errors=True)
|
|
release_dir.mkdir(parents=True)
|
|
archives = []
|
|
|
|
for platform in platforms:
|
|
platform_name = f"{platform['os']}-{platform['arch']}"
|
|
artifact_name = f"{APP_NAME}-{platform_name}"
|
|
binary_src = binary_artifacts_dir / artifact_name / platform["binary"]
|
|
if not binary_src.is_file():
|
|
raise SystemExit(f"Missing binary artifact: {binary_src}")
|
|
|
|
shutil.rmtree(package_dir, ignore_errors=True)
|
|
package_root = package_dir / APP_NAME
|
|
web_dist = package_root / "web" / "dist"
|
|
web_dist.mkdir(parents=True)
|
|
|
|
binary_dest = package_root / platform["binary"]
|
|
shutil.copy2(binary_src, binary_dest)
|
|
if platform["os"] != "windows":
|
|
binary_dest.chmod(0o755)
|
|
shutil.copytree(frontend_dir, web_dist, dirs_exist_ok=True)
|
|
|
|
archive_name = f"{APP_NAME}_{version}_{platform['os']}_{platform['arch']}.{platform['archive']}"
|
|
archive_path = release_dir / archive_name
|
|
if platform["archive"] == "tar.gz":
|
|
with tarfile.open(archive_path, "w:gz") as archive:
|
|
archive.add(package_root, arcname=APP_NAME)
|
|
elif platform["archive"] == "zip":
|
|
with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as archive:
|
|
for path in sorted(package_root.rglob("*")):
|
|
if path.is_file():
|
|
archive.write(path, path.relative_to(package_dir))
|
|
else:
|
|
raise SystemExit(f"Unsupported archive format: {platform['archive']}")
|
|
archives.append(archive_path)
|
|
|
|
checksum_path = release_dir / "checksums.txt"
|
|
with checksum_path.open("w", encoding="utf-8") as file:
|
|
for archive in archives:
|
|
file.write(f"{sha256(archive)} {archive.name}\n")
|
|
PY
|
|
|
|
- name: Generate release asset provenance
|
|
uses: actions/attest@v4
|
|
with:
|
|
subject-path: dist/release/*
|
|
|
|
- name: Upload release assets
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-assets
|
|
path: dist/release
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
build-docker-image:
|
|
name: build-docker-image (${{ matrix.dockerPlatform }})
|
|
needs: [release-info, build-frontend, build-docker-binary]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.release-info.outputs.docker_matrix) }}
|
|
runs-on: ${{ matrix.runner }}
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
id-token: write
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.release-info.outputs.tag }}
|
|
|
|
- name: Download frontend artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: frontend-dist
|
|
path: dist/docker/web/dist
|
|
|
|
- name: Download Docker binary artifacts
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: codex-proxy-rs-${{ matrix.os }}-${{ matrix.arch }}
|
|
path: dist/docker-artifacts/codex-proxy-rs-${{ matrix.os }}-${{ matrix.arch }}
|
|
|
|
- name: Prepare Docker prebuilt assets
|
|
shell: bash
|
|
env:
|
|
MATRIX_OS: ${{ matrix.os }}
|
|
MATRIX_ARCH: ${{ matrix.arch }}
|
|
MATRIX_BINARY: ${{ matrix.binary }}
|
|
run: |
|
|
set -euo pipefail
|
|
python3 - <<'PY'
|
|
import shutil
|
|
import os
|
|
import pathlib
|
|
|
|
app_name = "codex-proxy-rs"
|
|
platform_name = f"{os.environ['MATRIX_OS']}-{os.environ['MATRIX_ARCH']}"
|
|
binary_src = pathlib.Path("dist/docker-artifacts") / f"{app_name}-{platform_name}" / os.environ["MATRIX_BINARY"]
|
|
if not binary_src.is_file():
|
|
raise SystemExit(f"Missing Docker binary artifact: {binary_src}")
|
|
|
|
target_dir = pathlib.Path("dist/docker") / platform_name
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
binary_dest = target_dir / app_name
|
|
shutil.copy2(binary_src, binary_dest)
|
|
binary_dest.chmod(0o755)
|
|
PY
|
|
|
|
- name: Setup Docker Buildx
|
|
uses: docker/setup-buildx-action@v4
|
|
|
|
- name: Login to GHCR
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Build and push app image
|
|
id: build-and-push
|
|
uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
file: deploy/Dockerfile
|
|
target: runtime-prebuilt
|
|
platforms: ${{ matrix.dockerPlatform }}
|
|
push: true
|
|
build-args: |
|
|
CPR_PREBUILT_DIR=dist/docker
|
|
cache-from: type=gha,scope=runtime-prebuilt-${{ matrix.os }}-${{ matrix.arch }}
|
|
cache-to: type=gha,mode=max,scope=runtime-prebuilt-${{ matrix.os }}-${{ matrix.arch }}
|
|
provenance: mode=max
|
|
sbom: true
|
|
labels: |
|
|
org.opencontainers.image.title=Codex Proxy RS
|
|
org.opencontainers.image.version=${{ needs.release-info.outputs.version }}
|
|
org.opencontainers.image.revision=${{ needs.release-info.outputs.git_sha }}
|
|
org.opencontainers.image.source=https://github.com/zyycn/codex-proxy-rs
|
|
tags: |
|
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.release-info.outputs.version }}-${{ matrix.arch }}
|
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ needs.release-info.outputs.git_sha }}-${{ matrix.arch }}
|
|
|
|
publish:
|
|
needs: [release-info, package-assets, build-docker-image]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
packages: write
|
|
id-token: write
|
|
attestations: write
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.release-info.outputs.tag }}
|
|
|
|
- name: Download release assets
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: release-assets
|
|
path: dist/release
|
|
|
|
- name: Setup Docker Buildx
|
|
uses: docker/setup-buildx-action@v4
|
|
|
|
- name: Install Cosign
|
|
uses: sigstore/cosign-installer@v4.1.2
|
|
|
|
- name: Login to GHCR
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Create app image manifest
|
|
id: manifest
|
|
shell: bash
|
|
env:
|
|
VERSION: ${{ needs.release-info.outputs.version }}
|
|
GIT_SHA: ${{ needs.release-info.outputs.git_sha }}
|
|
DOCKER_PLATFORMS: ${{ needs.release-info.outputs.docker_platforms }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
image="${REGISTRY}/${IMAGE_NAME}"
|
|
sources=()
|
|
IFS=',' read -ra platforms <<< "${DOCKER_PLATFORMS}"
|
|
for platform in "${platforms[@]}"; do
|
|
platform="${platform//[[:space:]]/}"
|
|
[ -n "${platform}" ] || continue
|
|
arch="${platform##*/}"
|
|
sources+=("${image}:${VERSION}-${arch}")
|
|
done
|
|
|
|
if [ "${#sources[@]}" -eq 0 ]; then
|
|
echo "missing Docker image sources"
|
|
exit 1
|
|
fi
|
|
|
|
docker buildx imagetools create \
|
|
-t "${image}:${VERSION}" \
|
|
-t "${image}:latest" \
|
|
-t "${image}:sha-${GIT_SHA}" \
|
|
"${sources[@]}"
|
|
|
|
inspect_output="$(docker buildx imagetools inspect "${image}:${VERSION}")"
|
|
platform_list="$(awk '$1 == "Platform:" { print $2 }' <<< "${inspect_output}")"
|
|
for platform in "${platforms[@]}"; do
|
|
if ! grep -Eq "^${platform}(/|$)" <<< "${platform_list}"; then
|
|
echo "image manifest is missing platform ${platform}"
|
|
echo "${inspect_output}"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
digest="$(docker buildx imagetools inspect "${image}:${VERSION}" --format '{{ .Manifest.Digest }}')"
|
|
if [ -z "${digest}" ]; then
|
|
echo "missing image digest"
|
|
exit 1
|
|
fi
|
|
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Generate container provenance attestation
|
|
uses: actions/attest@v4
|
|
with:
|
|
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
subject-digest: ${{ steps.manifest.outputs.digest }}
|
|
push-to-registry: true
|
|
|
|
- name: Sign app image
|
|
shell: bash
|
|
env:
|
|
DIGEST: ${{ steps.manifest.outputs.digest }}
|
|
IMAGE_TAGS: |
|
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.release-info.outputs.version }}
|
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ needs.release-info.outputs.git_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${DIGEST}" ]; then
|
|
echo "missing image digest"
|
|
exit 1
|
|
fi
|
|
|
|
images=()
|
|
while IFS= read -r tag; do
|
|
[ -n "${tag}" ] || continue
|
|
images+=("${tag}@${DIGEST}")
|
|
done <<< "${IMAGE_TAGS}"
|
|
|
|
cosign sign --yes "${images[@]}"
|
|
|
|
- name: Publish GitHub Release
|
|
uses: softprops/action-gh-release@v3
|
|
with:
|
|
tag_name: ${{ needs.release-info.outputs.tag }}
|
|
generate_release_notes: true
|
|
previous_tag: ${{ needs.release-info.outputs.previous_tag }}
|
|
body: |
|
|
## 📥 Installation
|
|
|
|
**Docker:**
|
|
```bash
|
|
# GitHub Container Registry
|
|
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.release-info.outputs.version }}
|
|
```
|
|
|
|
**Manual download:**
|
|
Download the appropriate archive for your platform from the assets below.
|
|
|
|
Release archives are covered by GitHub artifact attestations. Docker images are published with SBOM/provenance attestations and keyless cosign signatures.
|
|
|
|
## 📚 Documentation
|
|
|
|
- [GitHub Repository](https://github.com/${{ github.repository }})
|
|
- [Installation Guide](https://github.com/${{ github.repository }}/blob/main/deploy/README.md)
|
|
fail_on_unmatched_files: true
|
|
files: |
|
|
dist/release/*
|