Skip to content

Merge pull request #12 from hienao/dev #9

Merge pull request #12 from hienao/dev

Merge pull request #12 from hienao/dev #9

Workflow file for this run

name: Publish image
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag:
description: Existing version tag to publish (for example v1.2.0-beta.1 or v1.2.0)
required: true
type: string
permissions:
contents: read
concurrency:
group: publish-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
cancel-in-progress: false
jobs:
classify:
name: Validate release tag
runs-on: ubuntu-latest
outputs:
channel: ${{ steps.version.outputs.channel }}
environment: ${{ steps.version.outputs.environment }}
tag: ${{ steps.version.outputs.tag }}
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
fetch-depth: 0
- name: Classify and validate version
id: version
shell: bash
env:
REQUESTED_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
run: |
set -euo pipefail
tag="${REQUESTED_TAG}"
if [[ "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$ ]]; then
channel="beta"
environment="ghcr-beta"
elif [[ "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
channel="production"
environment="ghcr-production"
else
echo "Unsupported release tag: ${tag}" >&2
exit 1
fi
git fetch origin main:refs/remotes/origin/main --tags --force
tag_sha="$(git rev-parse "${tag}^{commit}")"
if ! git merge-base --is-ancestor "${tag_sha}" origin/main; then
echo "Release tag ${tag} does not point to a commit contained in main" >&2
exit 1
fi
echo "channel=${channel}" >> "${GITHUB_OUTPUT}"
echo "environment=${environment}" >> "${GITHUB_OUTPUT}"
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
echo "version=${tag#v}" >> "${GITHUB_OUTPUT}"
test:
name: Release tests
needs: classify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.classify.outputs.tag }}
- uses: actions/setup-go@v7
with:
go-version-file: backend/go.mod
cache-dependency-path: backend/go.sum
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Backend tests
working-directory: backend
run: go test -race ./...
- name: Backend static analysis
working-directory: backend
run: go vet ./...
- name: Frontend install
working-directory: frontend
run: npm ci
- name: Frontend tests
working-directory: frontend
run: npm test -- --run
- name: Frontend build
working-directory: frontend
run: npm run build
- name: Validate Compose
run: docker compose --env-file .env.example config
verify-image:
name: Container smoke test
needs: classify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.classify.outputs.tag }}
- uses: docker/setup-buildx-action@v4
- name: Build verification image
uses: docker/build-push-action@v7
with:
context: .
file: deploy/Dockerfile
platforms: linux/amd64
load: true
push: false
tags: oscraper:release-check
cache-from: type=gha,scope=oscraper-linux-amd64
cache-to: type=gha,mode=min,scope=oscraper-linux-amd64,ignore-error=true
- name: Verify container health
shell: bash
run: |
set -euo pipefail
host_uid="$(id -u)"
host_gid="$(id -g)"
data_dir="${RUNNER_TEMP}/oscraper-release-data-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
cache_dir="${RUNNER_TEMP}/oscraper-release-cache-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
media_dir="${RUNNER_TEMP}/oscraper-release-media-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
mkdir -p "${data_dir}" "${cache_dir}" "${media_dir}"
media_owner_before="$(stat -c '%u:%g' "${media_dir}")"
cleanup() {
docker rm --force oscraper-release-check >/dev/null 2>&1 || true
}
trap cleanup EXIT
docker run --detach --name oscraper-release-check \
--env APP_ENV=production \
--env JWT_SECRET=release-check-jwt-secret-with-at-least-32-characters \
--env CREDENTIAL_ENCRYPTION_KEY=abcdef0123456789abcdef0123456789 \
--env PUID="${host_uid}" \
--env PGID="${host_gid}" \
--env UMASK=027 \
--volume "${data_dir}:/data" \
--volume "${cache_dir}:/cache" \
--volume "${media_dir}:/media" \
oscraper:release-check >/dev/null
for attempt in $(seq 1 30); do
if response="$(docker exec oscraper-release-check wget -qO- http://127.0.0.1:3113/api/health 2>/dev/null)"; then
echo "${response}"
echo "${response}" | grep -q '"status":"ok"'
docker exec oscraper-release-check sh -eu -c '
awk -v expected="$PUID" '\''$1 == "Uid:" { exit !($2 == expected) }'\'' /proc/1/status
awk -v expected="$PGID" '\''$1 == "Gid:" { exit !($2 == expected) }'\'' /proc/1/status
grep -Eq "^Umask:[[:space:]]+0027$" /proc/1/status
test "$(stat -c "%u:%g" /data/db)" = "$PUID:$PGID"
test "$(stat -c "%u:%g" /cache/logs)" = "$PUID:$PGID"
'
test "$(stat -c '%u:%g' "${media_dir}")" = "${media_owner_before}"
exit 0
fi
if [[ "$(docker inspect --format '{{.State.Running}}' oscraper-release-check)" != "true" ]]; then
docker logs oscraper-release-check
exit 1
fi
sleep 1
done
docker logs oscraper-release-check
exit 1
build-platform:
name: Build ${{ matrix.platform }} image
needs: [classify, test, verify-image]
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-24.04
artifact: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
artifact: arm64
runs-on: ${{ matrix.runner }}
environment: ${{ needs.classify.outputs.environment }}
permissions:
contents: read
packages: write
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/oscraper
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.classify.outputs.tag }}
- uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Protect immutable version tag
shell: bash
env:
VERSION: ${{ needs.classify.outputs.version }}
run: |
if docker buildx imagetools inspect "${IMAGE}:${VERSION}" >/dev/null 2>&1; then
echo "Image ${IMAGE}:${VERSION} already exists and will not be overwritten" >&2
exit 1
fi
- name: Generate image metadata
uses: docker/metadata-action@v6
id: meta
with:
images: ${{ env.IMAGE }}
flavor: |
latest=false
tags: |
type=raw,value=${{ needs.classify.outputs.version }}
type=raw,value=beta,enable=${{ needs.classify.outputs.channel == 'beta' }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.classify.outputs.tag }},enable=${{ needs.classify.outputs.channel == 'production' }}
type=semver,pattern={{major}},value=${{ needs.classify.outputs.tag }},enable=${{ needs.classify.outputs.channel == 'production' }}
type=raw,value=latest,enable=${{ needs.classify.outputs.channel == 'production' }}
type=sha,prefix=sha-,format=long
- name: Build and push platform image by digest
uses: docker/build-push-action@v7
id: build
with:
context: .
file: deploy/Dockerfile
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=oscraper-${{ matrix.artifact }}
cache-to: type=gha,mode=min,scope=oscraper-${{ matrix.artifact }},ignore-error=true
provenance: mode=max
sbom: true
- name: Export platform digest
shell: bash
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
if [[ ! "${DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "Invalid platform digest: ${DIGEST}" >&2
exit 1
fi
mkdir -p "${RUNNER_TEMP}/digests"
touch "${RUNNER_TEMP}/digests/${DIGEST#sha256:}"
- name: Upload platform digest
uses: actions/upload-artifact@v7
with:
name: digests-${{ matrix.artifact }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
publish:
name: Publish ${{ needs.classify.outputs.channel }} manifest
needs: [classify, build-platform]
runs-on: ubuntu-24.04
environment: ${{ needs.classify.outputs.environment }}
permissions:
contents: read
packages: write
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/oscraper
steps:
- uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Protect immutable version tag
shell: bash
env:
VERSION: ${{ needs.classify.outputs.version }}
run: |
if docker buildx imagetools inspect "${IMAGE}:${VERSION}" >/dev/null 2>&1; then
echo "Image ${IMAGE}:${VERSION} already exists and will not be overwritten" >&2
exit 1
fi
- name: Download platform digests
uses: actions/download-artifact@v8
with:
pattern: digests-*
path: ${{ runner.temp }}/digests
merge-multiple: true
- name: Generate image metadata
uses: docker/metadata-action@v6
id: meta
with:
images: ${{ env.IMAGE }}
flavor: |
latest=false
tags: |
type=raw,value=${{ needs.classify.outputs.version }}
type=raw,value=beta,enable=${{ needs.classify.outputs.channel == 'beta' }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.classify.outputs.tag }},enable=${{ needs.classify.outputs.channel == 'production' }}
type=semver,pattern={{major}},value=${{ needs.classify.outputs.tag }},enable=${{ needs.classify.outputs.channel == 'production' }}
type=raw,value=latest,enable=${{ needs.classify.outputs.channel == 'production' }}
type=sha,prefix=sha-,format=long
- name: Create multi-platform manifest
shell: bash
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail
tag_args=()
while IFS= read -r tag; do
if [[ -n "${tag}" ]]; then
tag_args+=(--tag "${tag}")
fi
done <<< "${TAGS}"
digest_args=()
for digest_file in "${RUNNER_TEMP}"/digests/*; do
if [[ ! -f "${digest_file}" || ! "$(basename "${digest_file}")" =~ ^[0-9a-f]{64}$ ]]; then
echo "Invalid digest artifact: ${digest_file}" >&2
exit 1
fi
digest_args+=("${IMAGE}@sha256:$(basename "${digest_file}")")
done
if [[ "${#digest_args[@]}" -ne 2 ]]; then
echo "Expected two platform digests, found ${#digest_args[@]}" >&2
exit 1
fi
docker buildx imagetools create "${tag_args[@]}" "${digest_args[@]}"
- name: Verify published manifest
id: manifest
shell: bash
env:
VERSION: ${{ needs.classify.outputs.version }}
run: |
set -euo pipefail
manifest="$(docker buildx imagetools inspect "${IMAGE}:${VERSION}")"
echo "${manifest}"
grep -Eq 'Platform:[[:space:]]+linux/amd64' <<< "${manifest}"
grep -Eq 'Platform:[[:space:]]+linux/arm64' <<< "${manifest}"
digest="$(awk '$1 == "Digest:" { print $2; exit }' <<< "${manifest}")"
if [[ ! "${digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "Invalid published manifest digest: ${digest}" >&2
exit 1
fi
echo "digest=${digest}" >> "${GITHUB_OUTPUT}"
- name: Write release summary
shell: bash
env:
CHANNEL: ${{ needs.classify.outputs.channel }}
DIGEST: ${{ steps.manifest.outputs.digest }}
VERSION: ${{ needs.classify.outputs.version }}
run: |
{
echo "## OScraper image published"
echo
echo "- Channel: \`${CHANNEL}\`"
echo "- Image: \`${IMAGE}:${VERSION}\`"
echo "- Digest: \`${DIGEST}\`"
} >> "${GITHUB_STEP_SUMMARY}"