mirror of
https://github.com/owncloud/ocis
synced 2026-04-25 17:25:21 +02:00
feat: [OCISDEV-783] release pipeline (#12194)
* feat: Add basic GH Actions file * feat: [OCISDEV-783] release pipeline * feat: [OCISDEV-783] release pipeline * feat: [OCISDEV-783] release pipeline, DeepDiver's review comments * feat: [OCISDEV-783] release pipeline, DeepDiver's review comments * feat: [OCISDEV-783] release pipeline, assert release * feat: [OCISDEV-783] release pipeline, audit release * feat: [OCISDEV-783] release pipeline, bianaries * feat: [OCISDEV-783] release pipeline, bianaries * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, dev.1 * feat: [OCISDEV-783] release pipeline, trivy scan * feat: [OCISDEV-783] release pipeline, gh actions * trigger CI * feat: [OCISDEV-783] release pipeline, align versions * feat: [OCISDEV-783] release pipeline, scan message * feat: [OCISDEV-783] release pipeline, ack alpine scan * feat: [OCISDEV-783] release pipeline, align versions * feat: [OCISDEV-783] release pipeline, review * feat: [OCISDEV-783] release pipeline, review * feat: [OCISDEV-783] release pipeline, trivy scan --------- Co-authored-by: Lukas Schwarz <lukas.schwarz@kiteworks.com>
This commit is contained in:
419
.github/workflows/release.yml
vendored
Normal file
419
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,419 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
branches:
|
||||
- 'feat/release-pipeline**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_override:
|
||||
description: 'Version override (leave empty to auto-detect from latest tag)'
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
env:
|
||||
PRODUCTION_RELEASE_TAGS: '5.0,7,8'
|
||||
DOCKER_REPO_ROLLING: owncloud/ocis-rolling
|
||||
DOCKER_REPO_PRODUCTION: owncloud/ocis
|
||||
NODE_VERSION: '24'
|
||||
PNPM_VERSION: '10.28.1'
|
||||
|
||||
jobs:
|
||||
determine-release-type:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.info.outputs.version }}
|
||||
is_production: ${{ steps.info.outputs.is_production }}
|
||||
is_prerelease: ${{ steps.info.outputs.is_prerelease }}
|
||||
docker_repos: ${{ steps.info.outputs.docker_repos }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
if: ${{ github.ref_type == 'branch' || (github.event_name == 'workflow_dispatch' && inputs.version_override == '') }}
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- id: info
|
||||
run: |
|
||||
next_dev() {
|
||||
# If latest tag is already a pre-release (e.g. v8.0.2-dev.1), reuse its base
|
||||
# version (8.0.2-dev.1) so repeated branch runs don't bump the patch counter.
|
||||
# If latest tag is a production release (e.g. v8.0.1), increment patch (8.0.2-dev.1).
|
||||
local tag=$(git tag --sort=-version:refname | grep -m1 '^v' || echo "v0.0.0")
|
||||
local ver="${tag#v}"
|
||||
if [[ "$ver" == *"-"* ]]; then
|
||||
echo "${ver%%-*}-dev.1"
|
||||
else
|
||||
IFS='.' read -r M m p <<< "$ver"
|
||||
echo "${M}.${m}.$((p + 1))-dev.1"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
VERSION="${{ inputs.version_override }}"
|
||||
[[ -z "$VERSION" ]] && VERSION=$(next_dev)
|
||||
elif [[ "${{ github.ref_type }}" == "branch" ]]; then
|
||||
VERSION=$(next_dev)
|
||||
else
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
fi
|
||||
|
||||
IS_PRODUCTION=false
|
||||
for TAG in ${PRODUCTION_RELEASE_TAGS//,/ }; do
|
||||
[[ "$VERSION" == "$TAG"* ]] && IS_PRODUCTION=true && break
|
||||
done
|
||||
|
||||
[[ "$VERSION" == *"-"* ]] && IS_PRERELEASE=true || IS_PRERELEASE=false
|
||||
|
||||
if [[ "$IS_PRODUCTION" == "true" && "$IS_PRERELEASE" == "false" ]]; then
|
||||
REPOS=[\"$DOCKER_REPO_ROLLING\",\"$DOCKER_REPO_PRODUCTION\"]
|
||||
else
|
||||
REPOS=[\"$DOCKER_REPO_ROLLING\"]
|
||||
fi
|
||||
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "is_production=$IS_PRODUCTION" >> $GITHUB_OUTPUT
|
||||
echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT
|
||||
echo "docker_repos=$REPOS" >> $GITHUB_OUTPUT
|
||||
|
||||
generate-code:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- run: npm install --silent -g yarn npx --force
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- run: pnpm config set store-dir ./.pnpm-store && make ci-node-generate
|
||||
env:
|
||||
CHROMEDRIVER_SKIP_DOWNLOAD: 'true'
|
||||
- run: make ci-go-generate
|
||||
env:
|
||||
BUF_TOKEN: ${{ secrets.BUF_API_TOKEN }}
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: generated-code
|
||||
path: |
|
||||
.
|
||||
!.git
|
||||
retention-days: 1
|
||||
|
||||
docker-build:
|
||||
name: docker-build (${{ matrix.arch }}, ${{ matrix.repo }})
|
||||
runs-on: ${{ matrix.arch == 'amd64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }}
|
||||
needs: [determine-release-type, generate-code]
|
||||
outputs:
|
||||
digest-amd64-rolling: ${{ steps.digest.outputs.digest-amd64-rolling }}
|
||||
digest-arm64-rolling: ${{ steps.digest.outputs.digest-arm64-rolling }}
|
||||
digest-amd64: ${{ steps.digest.outputs.digest-amd64 }}
|
||||
digest-arm64: ${{ steps.digest.outputs.digest-arm64 }}
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
repo: ${{ fromJSON(needs.determine-release-type.outputs.docker_repos) }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: generated-code
|
||||
path: .
|
||||
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- run: sudo apt-get update -q && sudo apt-get install -qy libvips libvips-dev
|
||||
- run: make -C ocis release-linux-docker-${{ matrix.arch }}
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
GOOS: linux
|
||||
ENABLE_VIPS: true
|
||||
- id: tags
|
||||
run: |
|
||||
VERSION="${{ needs.determine-release-type.outputs.version }}"
|
||||
REPO="${{ matrix.repo }}"
|
||||
ARCH="${{ matrix.arch }}"
|
||||
printf "tags<<EOF\n%s\nEOF\n" \
|
||||
"${REPO}:${VERSION}-linux-${ARCH}" >> $GITHUB_OUTPUT
|
||||
- id: build
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: ocis
|
||||
file: ocis/docker/Dockerfile.linux.${{ matrix.arch }}
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
push: true
|
||||
provenance: false
|
||||
build-args: |
|
||||
REVISION=${{ github.sha }}
|
||||
VERSION=${{ needs.determine-release-type.outputs.version }}
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
- id: digest
|
||||
run: |
|
||||
[[ "${{ matrix.repo }}" == *"rolling"* ]] \
|
||||
&& echo "digest-${{ matrix.arch }}-rolling=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT \
|
||||
|| echo "digest-${{ matrix.arch }}=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT
|
||||
|
||||
docker-scan:
|
||||
name: docker-scan (${{ matrix.arch }}, ${{ matrix.repo }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: [determine-release-type, docker-build]
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64]
|
||||
repo: ${{ fromJSON(needs.determine-release-type.outputs.docker_repos) }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
|
||||
id: trivy
|
||||
continue-on-error: true
|
||||
with:
|
||||
image-ref: ${{ matrix.repo }}:${{ needs.determine-release-type.outputs.version }}-linux-${{ matrix.arch }}
|
||||
format: table
|
||||
exit-code: 1
|
||||
severity: HIGH,CRITICAL
|
||||
ignore-unfixed: true
|
||||
skip-files: /usr/bin/gomplate,/usr/bin/wait-for
|
||||
hide-progress: true
|
||||
env:
|
||||
TRIVY_IGNOREFILE: .trivyignore
|
||||
|
||||
- name: Block on vulnerabilities
|
||||
if: steps.trivy.outcome == 'failure'
|
||||
run: |
|
||||
echo "::error title=Security scan blocked release::Image ${{ matrix.repo }}:${{ needs.determine-release-type.outputs.version }}-linux-${{ matrix.arch }} has HIGH or CRITICAL vulnerabilities (see Trivy report above). Fix all findings before releasing."
|
||||
exit 1
|
||||
|
||||
docker-manifest:
|
||||
name: docker-manifest (${{ matrix.repo }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: [determine-release-type, docker-build]
|
||||
strategy:
|
||||
matrix:
|
||||
repo: ${{ fromJSON(needs.determine-release-type.outputs.docker_repos) }}
|
||||
steps:
|
||||
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- if: ${{ contains(matrix.repo, 'rolling') }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t "${{ matrix.repo }}:${{ needs.determine-release-type.outputs.version }}" \
|
||||
"${{ needs.docker-build.outputs.digest-amd64-rolling }}" \
|
||||
"${{ needs.docker-build.outputs.digest-arm64-rolling }}"
|
||||
- if: ${{ !contains(matrix.repo, 'rolling') }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t "${{ matrix.repo }}:${{ needs.determine-release-type.outputs.version }}" \
|
||||
"${{ needs.docker-build.outputs.digest-amd64 }}" \
|
||||
"${{ needs.docker-build.outputs.digest-arm64 }}"
|
||||
|
||||
docker-readme:
|
||||
name: docker-readme (${{ matrix.repo }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: [determine-release-type, docker-manifest]
|
||||
strategy:
|
||||
matrix:
|
||||
repo: ${{ fromJSON(needs.determine-release-type.outputs.docker_repos) }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
readme-filepath: ocis/README.md
|
||||
|
||||
build-binaries:
|
||||
name: build-binaries (${{ matrix.os }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: [determine-release-type, generate-code]
|
||||
strategy:
|
||||
matrix:
|
||||
os: [linux, darwin]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: generated-code
|
||||
path: .
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- run: |
|
||||
make -C ocis release-${{ matrix.os }} OUTPUT=${{ needs.determine-release-type.outputs.version }}
|
||||
make -C ocis release-finish
|
||||
if [[ "${{ matrix.os }}" == "linux" ]]; then
|
||||
cp assets/End-User-License-Agreement-for-ownCloud-Infinite-Scale.pdf ocis/dist/release/
|
||||
fi
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: binaries-${{ matrix.os }}
|
||||
path: ocis/dist/release/*
|
||||
retention-days: 1
|
||||
|
||||
security-scan-trivy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
|
||||
with:
|
||||
scan-type: fs
|
||||
format: table
|
||||
exit-code: 0
|
||||
severity: CRITICAL,HIGH
|
||||
|
||||
license-check:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [determine-release-type, generate-code]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: generated-code
|
||||
path: .
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- run: npm install --silent -g yarn npx "pnpm@$PNPM_VERSION" --force
|
||||
- run: make ci-node-check-licenses && make ci-node-save-licenses
|
||||
- run: make ci-go-check-licenses && make ci-go-save-licenses
|
||||
- run: tar -czf third-party-licenses.tar.gz -C third-party-licenses .
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: third-party-licenses
|
||||
path: third-party-licenses.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
generate-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [determine-release-type]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- run: |
|
||||
make changelog CHANGELOG_VERSION=$(echo "${{ needs.determine-release-type.outputs.version }}" | cut -d'-' -f1)
|
||||
# calens produces empty output when no versioned changelog directory exists (e.g. dev builds).
|
||||
# Fall back to a pointer to the unreleased entries.
|
||||
if [[ $(wc -l < ocis/dist/CHANGELOG.md) -lt 5 ]]; then
|
||||
echo "Development release — no release notes yet. See [unreleased changes](https://github.com/owncloud/ocis/tree/master/changelog/unreleased/)." > ocis/dist/CHANGELOG.md
|
||||
fi
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: changelog
|
||||
path: ocis/dist/CHANGELOG.md
|
||||
retention-days: 1
|
||||
|
||||
create-github-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [determine-release-type, build-binaries, license-check, generate-changelog]
|
||||
steps:
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: binaries-linux
|
||||
path: release-assets
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: binaries-darwin
|
||||
path: release-assets
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: third-party-licenses
|
||||
path: release-assets
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: changelog
|
||||
path: .
|
||||
- uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
tag_name: v${{ needs.determine-release-type.outputs.version }}
|
||||
name: v${{ needs.determine-release-type.outputs.version }}
|
||||
body_path: CHANGELOG.md
|
||||
prerelease: ${{ needs.determine-release-type.outputs.is_prerelease == 'true' }}
|
||||
files: release-assets/*
|
||||
|
||||
audit-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- determine-release-type
|
||||
- generate-code
|
||||
- docker-build
|
||||
- docker-scan
|
||||
- docker-manifest
|
||||
- docker-readme
|
||||
- build-binaries
|
||||
- security-scan-trivy
|
||||
- license-check
|
||||
- generate-changelog
|
||||
- create-github-release
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: binaries-linux
|
||||
path: release-assets
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: binaries-darwin
|
||||
path: release-assets
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: third-party-licenses
|
||||
path: release-assets
|
||||
- run: |
|
||||
python3 scripts/audit-release.py \
|
||||
--version "${{ needs.determine-release-type.outputs.version }}" \
|
||||
--dir release-assets/ \
|
||||
--github-release --docker
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
needs:
|
||||
- determine-release-type
|
||||
- generate-code
|
||||
- docker-build
|
||||
- docker-scan
|
||||
- docker-manifest
|
||||
- docker-readme
|
||||
- build-binaries
|
||||
- security-scan-trivy
|
||||
- license-check
|
||||
- generate-changelog
|
||||
- create-github-release
|
||||
- audit-release
|
||||
steps:
|
||||
- run: |
|
||||
[[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]] \
|
||||
&& STATUS="FAILURE" || STATUS="SUCCESS"
|
||||
[[ "${{ github.event_name }}" == "schedule" ]] \
|
||||
&& SOURCE="nightly-${{ github.ref_name }}" \
|
||||
|| SOURCE="${{ github.ref_type == 'tag' && format('tag {0}', github.ref_name) || github.ref_name }}"
|
||||
SHA="${{ github.sha }}"
|
||||
MSG="${STATUS} [${{ github.repository }}#${SHA:0:8}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) (${SOURCE}) by **${{ github.triggering_actor }}**"
|
||||
[[ -z "${{ secrets.MATRIX_HOMESERVER }}" ]] && { echo "MATRIX_HOMESERVER not set, skipping notification"; exit 0; }
|
||||
curl -sf -X PUT \
|
||||
-H "Authorization: Bearer ${{ secrets.MATRIX_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"msgtype\":\"m.text\",\"body\":\"${MSG}\"}" \
|
||||
"${{ secrets.MATRIX_HOMESERVER }}/_matrix/client/r0/rooms/${{ secrets.MATRIX_ROOMID }}/send/m.room.message/$(date +%s)"
|
||||
11
.trivyignore
Normal file
11
.trivyignore
Normal file
@@ -0,0 +1,11 @@
|
||||
# Trivy vulnerability ignore file
|
||||
# Add CVE IDs or file paths here to suppress known/accepted findings.
|
||||
# See: https://aquasecurity.github.io/trivy/latest/docs/configuration/filtering/#trivyignore
|
||||
|
||||
# Alpine 3.23.3 ships vulnerable package versions; no fixed base image exists yet.
|
||||
# Fix: bump FROM alpine:3.23.3 → alpine:3.23.4 once released, or add
|
||||
# RUN apk upgrade --no-cache
|
||||
# to Dockerfile.linux.amd64 and Dockerfile.linux.arm64.
|
||||
CVE-2026-28390 # libcrypto3/libssl3 3.5.5-r0 → fixed in 3.5.6-r0 (openssl DoS)
|
||||
CVE-2026-22184 # zlib 1.3.1-r2 → fixed in 1.3.2-r0 (buffer overflow in untgz)
|
||||
CVE-2026-40200 # musl/musl-utils 1.2.5-r21 → fixed in 1.2.5-r23 (stack-based arbitrary code execution / DoS)
|
||||
235
scripts/audit-release.py
Executable file
235
scripts/audit-release.py
Executable file
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
BINARY_PLATFORMS = ["darwin-amd64", "darwin-arm64", "linux-386", "linux-amd64", "linux-arm", "linux-arm64"]
|
||||
MACHO_MAGIC = b"\xcf\xfa\xed\xfe"
|
||||
ELF_MAGIC = b"\x7fELF"
|
||||
BINARY_REF = {
|
||||
"linux-amd64": (ELF_MAGIC, 64, 0x3e),
|
||||
"linux-arm64": (ELF_MAGIC, 64, 0xb7),
|
||||
"linux-arm": (ELF_MAGIC, 32, 0x28),
|
||||
"linux-386": (ELF_MAGIC, 32, 0x03),
|
||||
"darwin-amd64": (MACHO_MAGIC, 64, 0x01000007),
|
||||
"darwin-arm64": (MACHO_MAGIC, 64, 0x0100000c),
|
||||
}
|
||||
EULA = "End-User-License-Agreement-for-ownCloud-Infinite-Scale.pdf"
|
||||
LICENSES = "third-party-licenses.tar.gz"
|
||||
PROD_TAGS = ("5.0", "7", "8")
|
||||
|
||||
passed = failed = 0
|
||||
|
||||
def ok(msg):
|
||||
global passed; passed += 1; print(f"[PASS] {msg}")
|
||||
|
||||
def fail(msg):
|
||||
global failed; failed += 1; print(f"[FAIL] {msg}", file=sys.stderr)
|
||||
|
||||
def expected_files(v):
|
||||
return [f for p in BINARY_PLATFORMS for f in (f"ocis-{v}-{p}", f"ocis-{v}-{p}.sha256")] + [EULA, LICENSES]
|
||||
|
||||
def is_production(v):
|
||||
return any(v.startswith(t) for t in PROD_TAGS) and "-" not in v
|
||||
|
||||
def run(cmd):
|
||||
return subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
def gh_api(path, token):
|
||||
req = urllib.request.Request(
|
||||
f"https://api.github.com{path}",
|
||||
headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"},
|
||||
)
|
||||
with urllib.request.urlopen(req) as r:
|
||||
return json.load(r)
|
||||
|
||||
|
||||
def check_local(directory, version):
|
||||
expected = expected_files(version)
|
||||
present = {f.name for f in directory.iterdir()}
|
||||
missing = [f for f in expected if f not in present]
|
||||
extra = present - set(expected)
|
||||
if missing: fail(f"file set: missing {missing}")
|
||||
else: ok(f"file set: {len(expected)} files present")
|
||||
if extra: fail(f"file set: unexpected {extra}")
|
||||
|
||||
for platform in BINARY_PLATFORMS:
|
||||
bin_path = directory / f"ocis-{version}-{platform}"
|
||||
sha_path = directory / f"ocis-{version}-{platform}.sha256"
|
||||
|
||||
if bin_path.exists():
|
||||
magic, bits, machine = BINARY_REF[platform]
|
||||
if bin_path.stat().st_size == 0:
|
||||
fail(f"{bin_path.name}: empty file")
|
||||
else:
|
||||
h = bin_path.read_bytes()[:20]
|
||||
if not h.startswith(magic):
|
||||
fail(f"{bin_path.name}: wrong magic {h[:4].hex()}")
|
||||
elif magic == ELF_MAGIC:
|
||||
cls, mach = h[4], struct.unpack_from("<H", h, 18)[0]
|
||||
if (64 if cls == 2 else 32) != bits or mach != machine:
|
||||
fail(f"{bin_path.name}: wrong ELF class={cls} machine=0x{mach:x}")
|
||||
else:
|
||||
ok(f"{bin_path.name}: ELF {bits}-bit 0x{mach:x}")
|
||||
else:
|
||||
cputype = struct.unpack_from("<I", h, 4)[0]
|
||||
if cputype != machine:
|
||||
fail(f"{bin_path.name}: wrong Mach-O cputype 0x{cputype:08x}")
|
||||
else:
|
||||
ok(f"{bin_path.name}: Mach-O 0x{cputype:08x}")
|
||||
|
||||
if sha_path.exists():
|
||||
if not bin_path.exists():
|
||||
fail(f"{sha_path.name}: binary missing")
|
||||
continue
|
||||
parts = sha_path.read_text().strip().split(" ", 1)
|
||||
if len(parts) != 2:
|
||||
fail(f"{sha_path.name}: bad format")
|
||||
continue
|
||||
rec_hash, rec_name = parts
|
||||
if rec_name != bin_path.name:
|
||||
fail(f"{sha_path.name}: filename mismatch '{rec_name}'")
|
||||
actual = hashlib.sha256(bin_path.read_bytes()).hexdigest()
|
||||
if actual != rec_hash:
|
||||
fail(f"{sha_path.name}: hash mismatch\n want: {rec_hash}\n got: {actual}")
|
||||
else:
|
||||
ok(f"{sha_path.name}: hash ok")
|
||||
|
||||
lic = directory / LICENSES
|
||||
if lic.exists():
|
||||
magic, size = lic.read_bytes()[:2], lic.stat().st_size
|
||||
if magic != b"\x1f\x8b": fail(f"{LICENSES}: not gzip ({magic.hex()})")
|
||||
elif size < 100_000: fail(f"{LICENSES}: too small ({size:,} bytes)")
|
||||
else: ok(f"{LICENSES}: gzip {size:,} bytes")
|
||||
|
||||
eula = directory / EULA
|
||||
if eula.exists():
|
||||
magic, size = eula.read_bytes()[:4], eula.stat().st_size
|
||||
if magic != b"%PDF": fail(f"{EULA}: not PDF ({magic})")
|
||||
elif size < 10_000: fail(f"{EULA}: too small ({size:,} bytes)")
|
||||
else: ok(f"{EULA}: PDF {size:,} bytes")
|
||||
|
||||
|
||||
def check_github_release(version):
|
||||
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
|
||||
if not token:
|
||||
fail("GH_TOKEN / GITHUB_TOKEN not set"); return
|
||||
try:
|
||||
r = gh_api(f"/repos/owncloud/ocis/releases/tags/v{version}", token)
|
||||
except urllib.error.HTTPError as e:
|
||||
fail(f"release v{version} not found: {e}"); return
|
||||
|
||||
checks = [
|
||||
(r.get("tag_name") == f"v{version}", f"tag_name: {r.get('tag_name')}"),
|
||||
(r.get("name") == f"v{version}", f"name: {r.get('name')}"),
|
||||
(not r.get("draft"), "draft: false"),
|
||||
(r.get("prerelease") == ("-" in version), f"prerelease: {r.get('prerelease')}"),
|
||||
]
|
||||
for passed_check, label in checks:
|
||||
ok(label) if passed_check else fail(label)
|
||||
|
||||
published = {a["name"] for a in r.get("assets", [])}
|
||||
expected = set(expected_files(version))
|
||||
missing, extra = expected - published, published - expected
|
||||
if missing: fail(f"assets missing: {sorted(missing)}")
|
||||
else: ok(f"assets: all {len(expected)} present")
|
||||
if extra: fail(f"assets unexpected: {sorted(extra)}")
|
||||
|
||||
|
||||
def check_docker(version):
|
||||
refs = [f"owncloud/ocis-rolling:{version}"]
|
||||
if is_production(version):
|
||||
refs.append(f"owncloud/ocis:{version}")
|
||||
for ref in refs:
|
||||
r = run(["docker", "buildx", "imagetools", "inspect", ref])
|
||||
if r.returncode != 0:
|
||||
fail(f"{ref}: {r.stderr.strip()}"); continue
|
||||
missing = [a for a in ("linux/amd64", "linux/arm64") if a not in r.stdout]
|
||||
if missing: fail(f"{ref}: missing {missing}")
|
||||
else: ok(f"{ref}: amd64+arm64 present")
|
||||
|
||||
|
||||
def resolve_run_id(branch, token):
|
||||
r = gh_api(f"/repos/owncloud/ocis/actions/workflows/release.yml/runs?branch={branch}&per_page=1", token)
|
||||
runs = r.get("workflow_runs", [])
|
||||
if not runs:
|
||||
sys.exit(f"no runs found for branch '{branch}'")
|
||||
run = runs[0]
|
||||
print(f"run {run['id']} {run['status']} {run['conclusion'] or 'in_progress'} {run['html_url']}")
|
||||
return str(run["id"])
|
||||
|
||||
|
||||
def check_run_artifacts(run_id):
|
||||
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
|
||||
if not token:
|
||||
fail("GH_TOKEN / GITHUB_TOKEN not set"); return
|
||||
try:
|
||||
r = gh_api(f"/repos/owncloud/ocis/actions/runs/{run_id}/artifacts", token)
|
||||
except urllib.error.HTTPError as e:
|
||||
fail(f"run {run_id}: {e}"); return
|
||||
|
||||
by_name = {a["name"]: a for a in r.get("artifacts", [])}
|
||||
for name in ("binaries-linux", "binaries-darwin", "third-party-licenses"):
|
||||
a = by_name.get(name)
|
||||
if not a: fail(f"{name}: missing"); continue
|
||||
if a.get("expired"): fail(f"{name}: expired"); continue
|
||||
if a["size_in_bytes"] == 0: fail(f"{name}: empty"); continue
|
||||
ok(f"{name}: {a['size_in_bytes']:,} bytes")
|
||||
|
||||
|
||||
def check_git(version):
|
||||
tag = f"v{version}"
|
||||
r = run(["git", "tag", "-v", tag])
|
||||
if r.returncode != 0: print(f"[WARN] {tag}: not a signed tag")
|
||||
else: ok(f"{tag}: signed tag ok")
|
||||
|
||||
r = run(["git", "cat-file", "-p", tag])
|
||||
if r.returncode != 0:
|
||||
fail(f"{tag}: cat-file failed")
|
||||
else:
|
||||
obj = next((l for l in r.stdout.splitlines() if l.startswith("object")), None)
|
||||
ok(f"{tag}: {obj}") if obj else fail(f"{tag}: no object line in cat-file")
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--version", required=False)
|
||||
p.add_argument("--run", metavar="RUN_ID")
|
||||
p.add_argument("--branch", metavar="BRANCH")
|
||||
p.add_argument("--dir", type=Path)
|
||||
p.add_argument("--github-release", action="store_true")
|
||||
p.add_argument("--docker", action="store_true")
|
||||
p.add_argument("--git", action="store_true")
|
||||
args = p.parse_args()
|
||||
|
||||
if not args.run and not args.branch and not args.version:
|
||||
p.error("--version is required unless --run or --branch is used")
|
||||
|
||||
if args.branch:
|
||||
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
|
||||
if not token: sys.exit("GH_TOKEN / GITHUB_TOKEN not set")
|
||||
args.run = resolve_run_id(args.branch, token)
|
||||
|
||||
needs_version = args.dir or args.github_release or args.docker or args.git
|
||||
if needs_version and not args.version:
|
||||
p.error("--version is required with --dir / --github-release / --docker / --git")
|
||||
|
||||
if args.run: check_run_artifacts(args.run)
|
||||
if args.dir: check_local(args.dir, args.version)
|
||||
if args.github_release: check_github_release(args.version)
|
||||
if args.docker: check_docker(args.version)
|
||||
if args.git: check_git(args.version)
|
||||
|
||||
print(f"\n{passed + failed} checks: {passed} passed, {failed} failed")
|
||||
sys.exit(1 if failed else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user