Skip to content

update procedure

update procedure #9

name: cockpit-plugins
on:
push:
branches:
- main
paths:
- "ansible/roles/cockpit_plugins/defaults/main.yml"
workflow_dispatch:
permissions:
contents: write
jobs:
check:
runs-on: ubuntu-24.04
outputs:
zfs_repo: ${{ steps.refs.outputs.zfs_repo }}
zfs_ref: ${{ steps.refs.outputs.zfs_ref }}
zfs_ref_safe: ${{ steps.refs.outputs.zfs_ref_safe }}
zfs_release: ${{ steps.refs.outputs.zfs_release }}
zfs_source_commit: ${{ steps.refs.outputs.zfs_source_commit }}
zfs_published: ${{ steps.published.outputs.zfs_published }}
s3_repo: ${{ steps.refs.outputs.s3_repo }}
s3_ref: ${{ steps.refs.outputs.s3_ref }}
s3_ref_safe: ${{ steps.refs.outputs.s3_ref_safe }}
s3_release: ${{ steps.refs.outputs.s3_release }}
s3_source_commit: ${{ steps.refs.outputs.s3_source_commit }}
s3_published: ${{ steps.published.outputs.s3_published }}
should_build: ${{ steps.published.outputs.should_build }}
steps:
- uses: actions/checkout@v4
- name: Resolve pinned plugin refs
id: refs
run: |
ruby <<'RUBY' >> "${GITHUB_OUTPUT}"
require "open3"
require "tmpdir"
require "yaml"
def sanitize(value)
value.gsub(/[^A-Za-z0-9._-]+/, "-")
end
def run!(*args)
stdout, stderr, status = Open3.capture3(*args)
return stdout if status.success?
abort "Command failed: #{args.join(" ")}\n#{stderr}#{stdout}"
end
def fetch_ref!(dir, repo, ref)
attempts = [
[ref],
["refs/tags/#{ref}:refs/tags/#{ref}"],
["refs/heads/#{ref}:refs/remotes/origin/#{ref}"]
]
errors = []
attempts.each do |refspec|
_stdout, stderr, status = Open3.capture3(
"git", "-C", dir, "fetch", "--depth=1", "origin", *refspec
)
return if status.success?
errors << stderr
end
abort "Unable to resolve #{repo}@#{ref}\n#{errors.join("\n")}"
end
def resolve_source_commit(repo, ref)
return ref.downcase if ref.match?(/\A[0-9a-f]{40}\z/)
Dir.mktmpdir("terrarium-cockpit-ref.") do |dir|
run!("git", "-C", dir, "init", "-q")
run!("git", "-C", dir, "remote", "add", "origin", repo)
fetch_ref!(dir, repo, ref)
run!("git", "-C", dir, "rev-parse", "FETCH_HEAD^{commit}").strip
end
end
defaults = YAML.load_file("ansible/roles/cockpit_plugins/defaults/main.yml")
{
"zfs" => {
"repo" => defaults.fetch("terrarium_cockpit_zfs_repo"),
"ref" => defaults.fetch("terrarium_cockpit_zfs_ref")
},
"s3" => {
"repo" => defaults.fetch("terrarium_cockpit_s3_repo"),
"ref" => defaults.fetch("terrarium_cockpit_s3_ref")
}
}.each do |plugin, values|
ref_safe = sanitize(values.fetch("ref"))
source_commit = resolve_source_commit(values.fetch("repo"), values.fetch("ref"))
puts "#{plugin}_repo=#{values.fetch("repo")}"
puts "#{plugin}_ref=#{values.fetch("ref")}"
puts "#{plugin}_ref_safe=#{ref_safe}"
puts "#{plugin}_release=cockpit-plugin-#{plugin}-#{ref_safe}"
puts "#{plugin}_source_commit=#{source_commit}"
end
RUBY
- name: Check reusable plugin releases
id: published
env:
GH_TOKEN: ${{ github.token }}
ZFS_REPO: ${{ steps.refs.outputs.zfs_repo }}
ZFS_REF: ${{ steps.refs.outputs.zfs_ref }}
ZFS_REF_SAFE: ${{ steps.refs.outputs.zfs_ref_safe }}
ZFS_RELEASE: ${{ steps.refs.outputs.zfs_release }}
ZFS_SOURCE_COMMIT: ${{ steps.refs.outputs.zfs_source_commit }}
S3_REPO: ${{ steps.refs.outputs.s3_repo }}
S3_REF: ${{ steps.refs.outputs.s3_ref }}
S3_REF_SAFE: ${{ steps.refs.outputs.s3_ref_safe }}
S3_RELEASE: ${{ steps.refs.outputs.s3_release }}
S3_SOURCE_COMMIT: ${{ steps.refs.outputs.s3_source_commit }}
run: |
set -euo pipefail
release_is_reusable() {
local plugin="$1"
local release="$2"
local source_repo="$3"
local source_ref="$4"
local source_commit="$5"
local ref_safe="$6"
local tmp
tmp="$(mktemp -d)"
if ! gh release view "${release}" \
--repo "${GITHUB_REPOSITORY}" \
--json author,isDraft,isPrerelease,assets \
> "${tmp}/release.json"
then
echo "Building ${release}; release does not exist."
rm -rf "${tmp}"
return 1
fi
if ! env \
PLUGIN="${plugin}" \
REF_SAFE="${ref_safe}" \
RELEASE_JSON="${tmp}/release.json" \
ruby <<'RUBY'
require "json"
plugin = ENV.fetch("PLUGIN")
ref_safe = ENV.fetch("REF_SAFE")
release = JSON.parse(File.read(ENV.fetch("RELEASE_JSON")))
author = release.dig("author", "login")
abort "release author #{author.inspect} is not github-actions[bot]" unless author == "github-actions[bot]"
abort "release is a draft" if release["isDraft"]
abort "release is a prerelease" if release["isPrerelease"]
asset_names = release.fetch("assets").map { |asset| asset.fetch("name") }
expected_assets = %w[amd64 arm64].flat_map do |arch|
basename = "cockpit-plugin-#{plugin}-#{ref_safe}-linux-#{arch}.tar.gz"
[basename, "#{basename}.sha256"]
end
missing = (["provenance.json"] + expected_assets) - asset_names
abort "missing release assets: #{missing.join(", ")}" unless missing.empty?
RUBY
then
echo "Building ${release}; release metadata is not trusted for reuse."
rm -rf "${tmp}"
return 1
fi
if ! gh release download "${release}" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "provenance.json" \
--dir "${tmp}" \
>/dev/null
then
echo "Building ${release}; provenance asset is missing."
rm -rf "${tmp}"
return 1
fi
if ! env \
PLUGIN="${plugin}" \
REF_SAFE="${ref_safe}" \
RELEASE="${release}" \
SOURCE_REPO="${source_repo}" \
SOURCE_REF="${source_ref}" \
SOURCE_COMMIT="${source_commit}" \
PROVENANCE="${tmp}/provenance.json" \
EXPECTED_ASSETS_FILE="${tmp}/expected-assets" \
ruby <<'RUBY'
require "json"
plugin = ENV.fetch("PLUGIN")
ref_safe = ENV.fetch("REF_SAFE")
expected_assets = %w[amd64 arm64].map do |arch|
"cockpit-plugin-#{plugin}-#{ref_safe}-linux-#{arch}.tar.gz"
end
provenance = JSON.parse(File.read(ENV.fetch("PROVENANCE")))
abort "unsupported provenance schema" unless provenance["schema_version"] == 1
abort "plugin mismatch" unless provenance["plugin"] == plugin
abort "release mismatch" unless provenance["release"] == ENV.fetch("RELEASE")
source = provenance.fetch("source")
abort "source repo mismatch" unless source["repo"] == ENV.fetch("SOURCE_REPO")
abort "source ref mismatch" unless source["ref"] == ENV.fetch("SOURCE_REF")
abort "source commit mismatch" unless source["commit"] == ENV.fetch("SOURCE_COMMIT")
workflow = provenance.fetch("workflow")
abort "workflow repository mismatch" unless workflow["repository"] == ENV.fetch("GITHUB_REPOSITORY")
asset_names = provenance.fetch("assets").map { |asset| asset.fetch("name") }
abort "asset set mismatch" unless asset_names.sort == expected_assets.sort
File.write(ENV.fetch("EXPECTED_ASSETS_FILE"), expected_assets.join("\n") + "\n")
RUBY
then
echo "Building ${release}; provenance does not match the pinned source."
rm -rf "${tmp}"
return 1
fi
while IFS= read -r asset; do
[ -n "${asset}" ] || continue
if ! gh release download "${release}" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "${asset}" \
--dir "${tmp}" \
>/dev/null
then
echo "Building ${release}; ${asset} is missing."
rm -rf "${tmp}"
return 1
fi
if ! gh release download "${release}" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "${asset}.sha256" \
--dir "${tmp}" \
>/dev/null
then
echo "Building ${release}; ${asset}.sha256 is missing."
rm -rf "${tmp}"
return 1
fi
actual_sha="$(sha256sum "${tmp}/${asset}" | awk '{print tolower($1)}')"
sidecar_sha="$(awk '{print tolower($1); exit}' "${tmp}/${asset}.sha256")"
provenance_sha="$(env ASSET="${asset}" PROVENANCE="${tmp}/provenance.json" ruby <<'RUBY'
require "json"
provenance = JSON.parse(File.read(ENV.fetch("PROVENANCE")))
asset = provenance.fetch("assets").find { |entry| entry.fetch("name") == ENV.fetch("ASSET") }
abort "asset missing from provenance" unless asset
puts asset.fetch("sha256").downcase
RUBY
)"
if [ "${actual_sha}" != "${sidecar_sha}" ] || [ "${actual_sha}" != "${provenance_sha}" ]; then
echo "Building ${release}; checksum mismatch for ${asset}."
rm -rf "${tmp}"
return 1
fi
done < "${tmp}/expected-assets"
echo "Reusing ${release}; provenance and checksums match ${source_repo}@${source_commit}."
rm -rf "${tmp}"
return 0
}
zfs_published=false
s3_published=false
if release_is_reusable \
"zfs" \
"${ZFS_RELEASE}" \
"${ZFS_REPO}" \
"${ZFS_REF}" \
"${ZFS_SOURCE_COMMIT}" \
"${ZFS_REF_SAFE}"
then
zfs_published=true
fi
if release_is_reusable \
"s3" \
"${S3_RELEASE}" \
"${S3_REPO}" \
"${S3_REF}" \
"${S3_SOURCE_COMMIT}" \
"${S3_REF_SAFE}"
then
s3_published=true
fi
should_build=true
if [ "${zfs_published}" = true ] && [ "${s3_published}" = true ]; then
should_build=false
fi
{
echo "zfs_published=${zfs_published}"
echo "s3_published=${s3_published}"
echo "should_build=${should_build}"
} >> "${GITHUB_OUTPUT}"
build:
runs-on: ubuntu-24.04
needs: check
strategy:
fail-fast: false
matrix:
include:
- plugin: zfs
arch: amd64
- plugin: zfs
arch: arm64
- plugin: s3
arch: amd64
- plugin: s3
arch: arm64
steps:
- uses: actions/checkout@v4
if: >-
${{
(matrix.plugin == 'zfs' && needs.check.outputs.zfs_published != 'true') ||
(matrix.plugin == 's3' && needs.check.outputs.s3_published != 'true')
}}
- uses: docker/setup-qemu-action@v3
if: >-
${{
matrix.arch == 'arm64' &&
(
(matrix.plugin == 'zfs' && needs.check.outputs.zfs_published != 'true') ||
(matrix.plugin == 's3' && needs.check.outputs.s3_published != 'true')
)
}}
- name: Resolve bundle basename
id: bundle
if: >-
${{
(matrix.plugin == 'zfs' && needs.check.outputs.zfs_published != 'true') ||
(matrix.plugin == 's3' && needs.check.outputs.s3_published != 'true')
}}
env:
PLUGIN: ${{ matrix.plugin }}
TARGET_ARCH: ${{ matrix.arch }}
ZFS_REF_SAFE: ${{ needs.check.outputs.zfs_ref_safe }}
S3_REF_SAFE: ${{ needs.check.outputs.s3_ref_safe }}
run: |
case "${PLUGIN}" in
zfs) PLUGIN_REF_SAFE="${ZFS_REF_SAFE}" ;;
s3) PLUGIN_REF_SAFE="${S3_REF_SAFE}" ;;
esac
BASENAME="cockpit-plugin-${PLUGIN}-${PLUGIN_REF_SAFE}-linux-${TARGET_ARCH}.tar.gz"
echo "basename=${BASENAME}" >> "${GITHUB_OUTPUT}"
echo "artifact_name=${BASENAME%.tar.gz}" >> "${GITHUB_OUTPUT}"
- name: Build Cockpit plugin bundle
if: >-
${{
(matrix.plugin == 'zfs' && needs.check.outputs.zfs_published != 'true') ||
(matrix.plugin == 's3' && needs.check.outputs.s3_published != 'true')
}}
env:
PLUGIN: ${{ matrix.plugin }}
TARGET_ARCH: ${{ matrix.arch }}
BUNDLE_BASENAME: ${{ steps.bundle.outputs.basename }}
ZFS_REPO: ${{ needs.check.outputs.zfs_repo }}
ZFS_REF: ${{ needs.check.outputs.zfs_ref }}
ZFS_SOURCE_COMMIT: ${{ needs.check.outputs.zfs_source_commit }}
S3_REPO: ${{ needs.check.outputs.s3_repo }}
S3_REF: ${{ needs.check.outputs.s3_ref }}
S3_SOURCE_COMMIT: ${{ needs.check.outputs.s3_source_commit }}
run: |
case "${PLUGIN}" in
zfs)
PLUGIN_REPO="${ZFS_REPO}"
PLUGIN_REF="${ZFS_REF}"
PLUGIN_SOURCE_COMMIT="${ZFS_SOURCE_COMMIT}"
;;
s3)
PLUGIN_REPO="${S3_REPO}"
PLUGIN_REF="${S3_REF}"
PLUGIN_SOURCE_COMMIT="${S3_SOURCE_COMMIT}"
;;
esac
mkdir -p release-assets
chmod +x scripts/build-cockpit-plugins-bundle.sh
docker run --rm \
--platform "linux/${TARGET_ARCH}" \
-v "${PWD}:/workspace" \
-w /workspace \
ubuntu:24.04 \
bash -lc "./scripts/build-cockpit-plugins-bundle.sh \
--plugin ${PLUGIN@Q} \
--output /workspace/release-assets/${BUNDLE_BASENAME} \
--repo ${PLUGIN_REPO@Q} \
--ref ${PLUGIN_REF@Q} \
--expected-commit ${PLUGIN_SOURCE_COMMIT@Q}"
- uses: actions/upload-artifact@v4
if: >-
${{
(matrix.plugin == 'zfs' && needs.check.outputs.zfs_published != 'true') ||
(matrix.plugin == 's3' && needs.check.outputs.s3_published != 'true')
}}
with:
name: ${{ steps.bundle.outputs.artifact_name }}
path: release-assets/${{ steps.bundle.outputs.basename }}
if-no-files-found: error
publish:
runs-on: ubuntu-24.04
needs:
- check
- build
if: ${{ always() && needs.check.outputs.should_build == 'true' && needs.build.result == 'success' }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release assets
id: prepare
env:
ZFS_REF_SAFE: ${{ needs.check.outputs.zfs_ref_safe }}
ZFS_RELEASE: ${{ needs.check.outputs.zfs_release }}
ZFS_REPO: ${{ needs.check.outputs.zfs_repo }}
ZFS_REF: ${{ needs.check.outputs.zfs_ref }}
ZFS_SOURCE_COMMIT: ${{ needs.check.outputs.zfs_source_commit }}
S3_REF_SAFE: ${{ needs.check.outputs.s3_ref_safe }}
S3_RELEASE: ${{ needs.check.outputs.s3_release }}
S3_REPO: ${{ needs.check.outputs.s3_repo }}
S3_REF: ${{ needs.check.outputs.s3_ref }}
S3_SOURCE_COMMIT: ${{ needs.check.outputs.s3_source_commit }}
ZFS_PUBLISHED: ${{ needs.check.outputs.zfs_published }}
S3_PUBLISHED: ${{ needs.check.outputs.s3_published }}
run: |
set -euo pipefail
mkdir -p release-assets/zfs release-assets/s3
prepare_plugin() {
local plugin="$1"
local ref_safe="$2"
local release="$3"
local source_repo="$4"
local source_ref="$5"
local source_commit="$6"
local dir="release-assets/${plugin}"
local assets=()
local asset_list
mkdir -p "${dir}"
for arch in amd64 arm64; do
local basename="cockpit-plugin-${plugin}-${ref_safe}-linux-${arch}.tar.gz"
cp "artifacts/${basename%.tar.gz}/${basename}" "${dir}/${basename}"
(cd "${dir}" && sha256sum "${basename}" > "${basename}.sha256")
assets+=("${basename}")
done
asset_list="$(printf "%s\n" "${assets[@]}")"
env \
PLUGIN="${plugin}" \
RELEASE="${release}" \
SOURCE_REPO="${source_repo}" \
SOURCE_REF="${source_ref}" \
SOURCE_COMMIT="${source_commit}" \
ASSET_DIR="${dir}" \
ASSETS="${asset_list}" \
ruby <<'RUBY'
require "json"
asset_dir = ENV.fetch("ASSET_DIR")
assets = ENV.fetch("ASSETS").lines.map(&:strip).reject(&:empty?).map do |name|
{
"name" => name,
"sha256" => File.read(File.join(asset_dir, "#{name}.sha256")).split.first
}
end
provenance = {
"schema_version" => 1,
"plugin" => ENV.fetch("PLUGIN"),
"release" => ENV.fetch("RELEASE"),
"source" => {
"repo" => ENV.fetch("SOURCE_REPO"),
"ref" => ENV.fetch("SOURCE_REF"),
"commit" => ENV.fetch("SOURCE_COMMIT")
},
"workflow" => {
"repository" => ENV.fetch("GITHUB_REPOSITORY"),
"commit" => ENV.fetch("GITHUB_SHA"),
"run_id" => ENV.fetch("GITHUB_RUN_ID"),
"run_attempt" => ENV.fetch("GITHUB_RUN_ATTEMPT")
},
"assets" => assets
}
File.write(File.join(asset_dir, "provenance.json"), JSON.pretty_generate(provenance) + "\n")
RUBY
}
zfs_ready=false
s3_ready=false
if [ "${ZFS_PUBLISHED}" != true ]; then
prepare_plugin \
"zfs" \
"${ZFS_REF_SAFE}" \
"${ZFS_RELEASE}" \
"${ZFS_REPO}" \
"${ZFS_REF}" \
"${ZFS_SOURCE_COMMIT}"
zfs_ready=true
fi
if [ "${S3_PUBLISHED}" != true ]; then
prepare_plugin \
"s3" \
"${S3_REF_SAFE}" \
"${S3_RELEASE}" \
"${S3_REPO}" \
"${S3_REF}" \
"${S3_SOURCE_COMMIT}"
s3_ready=true
fi
{
echo "zfs_ready=${zfs_ready}"
echo "s3_ready=${s3_ready}"
} >> "${GITHUB_OUTPUT}"
- name: Publish cockpit-zfs bundles
if: steps.prepare.outputs.zfs_ready == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.check.outputs.zfs_release }}
target_commitish: ${{ github.sha }}
name: ${{ needs.check.outputs.zfs_release }}
files: |
release-assets/zfs/*.tar.gz
release-assets/zfs/*.tar.gz.sha256
release-assets/zfs/provenance.json
prerelease: false
make_latest: false
overwrite_files: true
- name: Publish cockpit-s3 bundles
if: steps.prepare.outputs.s3_ready == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.check.outputs.s3_release }}
target_commitish: ${{ github.sha }}
name: ${{ needs.check.outputs.s3_release }}
files: |
release-assets/s3/*.tar.gz
release-assets/s3/*.tar.gz.sha256
release-assets/s3/provenance.json
prerelease: false
make_latest: false
overwrite_files: true