Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 66 additions & 24 deletions .github/workflows/checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ jobs:
timeout-minutes: 20
env:
PROBE_ENDPOINT: http://localhost:8080/ga4gh/registry/v1/ui/
# Passed to the image build by Docker Compose, as the workflows do
SETUPTOOLS_SCM_PRETEND_VERSION: 0.0.0+compose
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -157,56 +159,96 @@ jobs:
run: |
curl --fail --silent --show-error --output /dev/null \
--header 'Accept: application/json' "${PROBE_ENDPOINT}"
- name: Check app version
run: |
actual="$(docker compose exec -T cloud-registry python -c \
"from importlib.metadata import version; print(version('cloud-registry'))")"
if [ "${actual}" != "${SETUPTOOLS_SCM_PRETEND_VERSION}" ]; then
echo "::error::App reports version '${actual}', expected '${SETUPTOOLS_SCM_PRETEND_VERSION}'"
exit 1
fi
- name: Show app logs
if: failure()
run: docker compose logs
publish:
name: Build and publish app image
runs-on: ubuntu-latest
timeout-minutes: 30
if: ${{ github.event_name == 'push' }}
needs: [analyze_code, tests_passed, helm, smoke_test]
# Docker Hub secrets; the environment only allows branch `dev`
environment: dev-images
# Publish one image at a time, and only for the newest commit on the
# branch, so that an older run cannot overwrite `latest` with older code
# branch, so that an older run cannot overwrite `dev` with older code
concurrency:
group: ${{ github.workflow }}-publish-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Check that commit is still the branch head
id: head
env:
REPOSITORY: ${{ github.repository }}
REF: ${{ github.ref }}
SHA: ${{ github.sha }}
run: |
head=$(git ls-remote "https://github.com/${{ github.repository }}.git" "${{ github.ref }}" | cut -f1)
[ -n "${head}" ] || { echo "::error::Could not look up ${{ github.ref }}"; exit 1; }
if [ "${head}" = "${{ github.sha }}" ]; then
head=$(git ls-remote "https://github.com/${REPOSITORY}.git" "${REF}" | cut -f1)
[ -n "${head}" ] || { echo "::error::Could not look up ${REF}"; exit 1; }
if [ "${head}" = "${SHA}" ]; then
echo "current=true" >> "$GITHUB_OUTPUT"
else
echo "::notice::Skipping publish: ${{ github.ref }} has moved on to ${head}"
echo "::notice::Skipping publish: ${REF} has moved on to ${head}"
fi
- name: Check out repository
if: steps.head.outputs.current == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Generate tag
# Full history and tags, to derive the version
fetch-depth: 0
- name: Set up Python
if: steps.head.outputs.current == 'true'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
# The Docker build gets no Git metadata, so the version (e.g.,
# `0.3.1.dev4+g<commit>`) is passed as a build argument
- name: Derive version
if: steps.head.outputs.current == 'true'
id: version
# Version pinned in `requirements_dev.txt` (as a constraint, so that
# only setuptools-scm is installed), where Dependabot updates it
run: |
echo "TAG=$(date '+%Y%m%d')" >> $GITHUB_ENV
- name: Build and publish image
pip install setuptools-scm -c requirements_dev.txt
echo "version=$(python -m setuptools_scm)" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
if: steps.head.outputs.current == 'true'
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Log in to Docker Hub
if: steps.head.outputs.current == 'true'
id: docker
uses: philips-software/docker-ci-scripts@69ca3b3e358f8746c67341d6ab551ef5ec25a36c # v5.0.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
dockerfile: .
image-name: "cloud-registry"
tags: "latest ${{ env.TAG }}"
push-branches: "${{ github.event.repository.default_branch }}"
env:
REGISTRY_USERNAME: ${{ secrets.DOCKERHUB_LOGIN }}
REGISTRY_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}"
DOCKER_ORGANIZATION: ${{ secrets.DOCKERHUB_ORG }}
GITHUB_ORGANIZATION: ${{ github.repository_owner }}
- name: Verify that image was pushed
username: ${{ secrets.DOCKERHUB_LOGIN }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Tags `dev` and `sha-<commit>` (never `latest`, which is for
# releases); OCI labels with the version, commit and source
- name: Generate image tags and labels
if: steps.head.outputs.current == 'true'
run: |
echo "Push indicator: ${{ steps.docker.outputs.push-indicator }}"
echo "# Set to 'true' if image was pushed, empty string otherwise"
test "${{ steps.docker.outputs.push-indicator }}" == "true"
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ secrets.DOCKERHUB_ORG }}/cloud-registry
flavor: latest=false
tags: |
type=raw,value=dev
type=sha,format=long
labels: |
org.opencontainers.image.version=${{ steps.version.outputs.version }}
- name: Build and push image
if: steps.head.outputs.current == 'true'
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: SETUPTOOLS_SCM_PRETEND_VERSION=${{ steps.version.outputs.version }}
218 changes: 218 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# Release when a version tag (`vX.Y.Z`, or `vX.Y.ZaN`, `vX.Y.ZbN`,
# `vX.Y.ZrcN` for pre-releases) is pushed: check the tag, package the Helm
# chart, build, check and push the Docker image, and create the GitHub
# release with the chart attached. The tag is the only source of the version:
# it is passed to the image build (the Docker build gets no Git metadata) and
# set as the chart's `version` and `appVersion`. Publishing runs in the
# `release` environment (required reviewer, Docker Hub secrets). Release tags
# are immutable (tag ruleset). See "Versioning and releases" in README.md.

name: Release

on:
push:
tags: ["v*"]

permissions:
contents: read

# One run per tag, so that a run waiting for approval is not cancelled by
# the run of another tag; `latest` is decided at the end of each run, from
# freshly fetched tags on `main`
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
prepare:
name: Check tag and package chart
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
version: ${{ steps.version.outputs.version }}
prerelease: ${{ steps.version.outputs.prerelease }}
sha: ${{ steps.version.outputs.sha }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Full history and all tags, to check that the release commit is on
# `main`
fetch-depth: 0
- name: Check release tag and commit
id: version
# Guards against accidental releases, e.g., of unmerged commits; the
# `release` environment's protection rules (required reviewer) are
# what actually control publishing
env:
TAG: ${{ github.ref_name }}
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
run: |
# Canonical versions only (no leading zeros), as setuptools-scm
# would normalize them
if [[ ! "${TAG}" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)((a|b|rc)(0|[1-9][0-9]*))?$ ]]; then
echo "::error::Tag '${TAG}' is not a release version (e.g., v1.2.3 or v1.2.3rc1)"
exit 1
fi
base="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
pre="${BASH_REMATCH[4]}"
# Annotated, with a signature that GitHub verified
if [[ "$(git cat-file -t "refs/tags/${TAG}")" != "tag" ]]; then
echo "::error::Tag '${TAG}' is not annotated (git tag -s)"
exit 1
fi
verified="$(gh api "repos/${REPOSITORY}/git/tags/$(git rev-parse "refs/tags/${TAG}")" \
--jq .verification.verified)"
if [[ "${verified}" != "true" ]]; then
echo "::error::Tag '${TAG}' has no signature verified by GitHub (git tag -s)"
exit 1
fi
if ! git merge-base --is-ancestor HEAD origin/main; then
echo "::error::Release commit is not on 'main'"
exit 1
fi
{
echo "version=${TAG#v}"
echo "sha=$(git rev-parse HEAD)"
# Charts need SemVer: `1.2.3rc1` becomes `1.2.3-rc1`
echo "chart_version=${base}${pre:+-${pre}}"
if [[ -n "${pre}" ]]; then
echo "prerelease=true"
else
echo "prerelease=false"
fi
} >> "${GITHUB_OUTPUT}"
# Helm is preinstalled on GitHub-hosted Ubuntu runners; `appVersion`
# (the default image tag) is the release version
- name: Package chart
env:
VERSION: ${{ steps.version.outputs.version }}
CHART_VERSION: ${{ steps.version.outputs.chart_version }}
run: |
helm lint deployment --strict -f deployment/ci/test-values.yaml
helm package deployment \
--version "${CHART_VERSION}" --app-version "${VERSION}" -d dist
ls "dist/cloud-registry-${CHART_VERSION}.tgz"
- name: Upload chart
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: chart
path: dist/
if-no-files-found: error

publish:
name: Publish image and release
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 30
environment:
name: release
url: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }}
permissions:
contents: write # create the GitHub release
env:
VERSION: ${{ needs.prepare.outputs.version }}
steps:
# The commit checked by `prepare`; release tags cannot be moved (tag
# ruleset)
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ needs.prepare.outputs.sha }}
# All tags and `main`, to determine the highest release
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
# Tag `X.Y.Z`; OCI labels with the version, commit and source
- name: Generate image tags and labels
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ secrets.DOCKERHUB_ORG }}/cloud-registry
flavor: latest=false
tags: type=raw,value=${{ needs.prepare.outputs.version }}
# Built once and loaded; the checked image is the one that is pushed
- name: Build image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
load: true
# Single image, as pushed with `docker push` (no attestations)
provenance: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: SETUPTOOLS_SCM_PRETEND_VERSION=${{ needs.prepare.outputs.version }}
- name: Check app version of image
env:
IMAGE: ${{ steps.meta.outputs.tags }}
run: |
actual="$(docker run --rm --entrypoint python "${IMAGE}" \
-c "from importlib.metadata import version; print(version('cloud-registry'))")"
if [[ "${actual}" != "${VERSION}" ]]; then
echo "::error::Image reports version '${actual}', expected '${VERSION}'"
exit 1
fi
- name: Log in to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKERHUB_LOGIN }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push image
env:
IMAGE: ${{ steps.meta.outputs.tags }}
run: docker push "${IMAGE}"
- name: Download chart
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: chart
path: dist/
# Decided last, from freshly fetched tags (the repository is public, so
# no credentials are needed), so that neither a re-run of an older
# release nor a concurrent release moves `latest` back
- name: Check whether this is the highest release
id: latest
env:
TAG: ${{ github.ref_name }}
PRERELEASE: ${{ needs.prepare.outputs.prerelease }}
run: |
git fetch --quiet --force --tags origin \
"+refs/heads/main:refs/remotes/origin/main"
# Stable releases on `main` only, so that stray tags are ignored
highest="$(git tag -l 'v*' --merged origin/main --sort=-v:refname \
| grep -E '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' \
| head -n 1 || true)"
if [[ "${PRERELEASE}" == "false" && "${highest}" == "${TAG}" ]]; then
echo "latest=true" >> "${GITHUB_OUTPUT}"
else
echo "latest=false" >> "${GITHUB_OUTPUT}"
fi
- name: Push image as latest
if: steps.latest.outputs.latest == 'true'
env:
IMAGE: ${{ steps.meta.outputs.tags }}
run: |
docker tag "${IMAGE}" "${IMAGE%:*}:latest"
docker push "${IMAGE%:*}:latest"
- name: Create release with generated notes
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
LATEST: ${{ steps.latest.outputs.latest }}
PRERELEASE: ${{ needs.prepare.outputs.prerelease }}
run: |
# On reruns, complete an existing release instead of failing
if gh release view "${TAG}" > /dev/null 2>&1; then
gh release upload "${TAG}" dist/* --clobber
gh release edit "${TAG}" --latest="${LATEST}"
exit 0
fi
gh release create "${TAG}" dist/* \
--verify-tag \
--title "${TAG}" \
--generate-notes \
--prerelease="${PRERELEASE}" \
--latest="${LATEST}"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,6 @@ tags
# Local config overrides
config.local.yaml
docker-compose.override.yaml

# Generated by sphinx-apidoc on docs builds
docs/api/modules/
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ RUN pip install --no-cache-dir -r /tmp/src/requirements.txt
## Install app (non-editable; the base config is installed with the package,
## and deployments override it via `FOCA_CONFIG_OVERLAY`)
## (`README.md` is the package's long description)
## The build context has no Git metadata, so the version is passed as a build
## argument (e.g., `--build-arg SETUPTOOLS_SCM_PRETEND_VERSION=0.3.0`, as the
## workflows do); without it, the app reports version `0.0.0.dev0`
COPY pyproject.toml README.md /tmp/src/
COPY cloud_registry /tmp/src/cloud_registry
ARG SETUPTOOLS_SCM_PRETEND_VERSION
RUN pip install --no-cache-dir --no-deps /tmp/src \
&& rm -rf /tmp/src
USER 1000
Expand Down
Loading
Loading