Skip to content

Commit c9a46e9

Browse files
authored
Merge pull request #10 from Keeper-Security/ci/docker-publish
Add Docker image release pipeline
2 parents f97bc82 + 49e8bed commit c9a46e9

12 files changed

Lines changed: 682 additions & 11 deletions

.dockerignore

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Keeps the build context small and prevents local credentials from being sent to the builder.
2+
3+
# VCS and CI
4+
.git
5+
.github
6+
.gitignore
7+
.dockerignore
8+
.idea
9+
.vscode
10+
.DS_Store
11+
12+
# Reinstalled from the lockfile inside the image
13+
node_modules
14+
15+
# Credentials
16+
config.yaml
17+
service-account.json
18+
ksm-config.json
19+
.env
20+
.env.*
21+
*.pem
22+
*.key
23+
24+
# Compose files carry a KSM config and API key
25+
docker-compose.yml
26+
docker-compose.example.yml
27+
28+
# Not needed at runtime
29+
scripts
30+
Taskfile.yml
31+
*.md
32+
*.log
33+
logs
34+
coverage
35+
dist

.github/workflows/ci.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Build verification for pull requests and the default branch.
2+
#
3+
# Both published architectures are built here, so an architecture-specific failure surfaces on
4+
# the pull request rather than during a release. Pushes to the default branch also export the
5+
# BuildKit cache that docker-release.yml consumes.
6+
7+
name: CI
8+
9+
on:
10+
pull_request:
11+
push:
12+
branches: [main]
13+
14+
permissions:
15+
contents: read
16+
17+
concurrency:
18+
group: ci-${{ github.ref }}
19+
cancel-in-progress: true
20+
21+
env:
22+
IMAGE_NAME: keeper/gchat-app
23+
24+
jobs:
25+
build:
26+
runs-on: ubuntu-latest
27+
timeout-minutes: 30
28+
steps:
29+
- name: Checkout repository
30+
uses: actions/checkout@v6
31+
32+
- name: Install dependencies from the lockfile
33+
run: npm ci --omit=dev
34+
35+
- name: Offline end-to-end flow
36+
run: npm run test:local
37+
38+
- name: Set up QEMU
39+
uses: docker/setup-qemu-action@v3
40+
41+
- name: Set up Docker Buildx
42+
uses: docker/setup-buildx-action@v3
43+
44+
- name: Build both published architectures
45+
uses: docker/build-push-action@v6
46+
with:
47+
context: .
48+
file: ./Dockerfile
49+
platforms: linux/amd64,linux/arm64
50+
push: false
51+
provenance: false
52+
sbom: false
53+
cache-from: type=gha,scope=gchat-app
54+
cache-to: ${{ github.ref == 'refs/heads/main' && 'type=gha,mode=max,scope=gchat-app' || '' }}
55+
56+
- name: Load the amd64 image for a smoke test
57+
uses: docker/build-push-action@v6
58+
with:
59+
context: .
60+
file: ./Dockerfile
61+
platforms: linux/amd64
62+
load: true
63+
push: false
64+
provenance: false
65+
sbom: false
66+
tags: ${{ env.IMAGE_NAME }}:ci
67+
cache-from: type=gha,scope=gchat-app
68+
69+
# The app exposes no HTTP surface, so the container-level assertion is that it starts
70+
# and exits with a clear error when configuration is missing.
71+
- name: Smoke test
72+
run: |
73+
set -euo pipefail
74+
docker run --rm --entrypoint node "${IMAGE_NAME}:ci" \
75+
-e 'console.log(process.arch, process.version)'
76+
out="$(docker run --rm "${IMAGE_NAME}:ci" 2>&1 || true)"
77+
if ! grep -q 'Missing configuration' <<<"$out"; then
78+
echo "::error::Container did not exit with the expected configuration error"
79+
printf '%s\n' "$out"
80+
exit 1
81+
fi
82+
echo "Smoke test OK"
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Points :latest at an already-published version tag.
2+
#
3+
# This is a registry-side manifest copy rather than a rebuild, so :latest resolves to the exact
4+
# digest that was verified.
5+
#
6+
# Run manually after the target tag has passed the checks in RELEASING.md.
7+
8+
name: Promote Docker Image to :latest
9+
10+
on:
11+
workflow_dispatch:
12+
inputs:
13+
version:
14+
description: 'Published version tag to promote (e.g. v1.0.0). Prereleases are rejected.'
15+
required: true
16+
type: string
17+
18+
permissions:
19+
contents: read
20+
21+
# Shared with docker-release.yml so a promotion cannot interleave with a build.
22+
concurrency:
23+
group: docker-release
24+
cancel-in-progress: false
25+
26+
env:
27+
IMAGE_NAME: keeper/gchat-app
28+
29+
jobs:
30+
promote:
31+
runs-on: ubuntu-latest
32+
timeout-minutes: 15
33+
environment: release
34+
permissions:
35+
contents: read
36+
steps:
37+
- name: Checkout repository
38+
uses: actions/checkout@v6
39+
with:
40+
fetch-depth: 0
41+
42+
- name: Validate promotion target
43+
env:
44+
VERSION: ${{ inputs.version }}
45+
run: |
46+
set -euo pipefail
47+
48+
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
49+
echo "::error::'$VERSION' is not a GA tag. :latest must not point at a prerelease."
50+
exit 1
51+
fi
52+
53+
if ! git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
54+
echo "::error::git tag $VERSION does not exist in this repository"
55+
exit 1
56+
fi
57+
58+
HIGHEST="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | grep -v -- '-' | sort -V | tail -1)"
59+
if [ "$VERSION" != "$HIGHEST" ]; then
60+
echo "::error::$VERSION is not the highest GA tag ($HIGHEST); refusing to move :latest backwards"
61+
exit 1
62+
fi
63+
echo "OK: $VERSION is the highest GA tag"
64+
65+
- name: Set up Docker Buildx
66+
uses: docker/setup-buildx-action@v3
67+
68+
- name: Log in to DockerHub
69+
uses: docker/login-action@v4
70+
with:
71+
username: ${{ secrets.DOCKERHUB_USERNAME }}
72+
password: ${{ secrets.DOCKERHUB_TOKEN }}
73+
74+
- name: Verify the source tag is a complete multi-arch image
75+
env:
76+
VERSION: ${{ inputs.version }}
77+
run: |
78+
set -euo pipefail
79+
SRC="${IMAGE_NAME}:${VERSION}"
80+
PLATFORMS="$(docker buildx imagetools inspect "$SRC" --raw \
81+
| jq -r '.manifests[] | select(.platform.architecture != "unknown")
82+
| "\(.platform.os)/\(.platform.architecture)"' | sort -u)"
83+
echo "Source platforms:"
84+
printf '%s\n' "$PLATFORMS"
85+
for required in linux/amd64 linux/arm64; do
86+
if ! grep -qx "$required" <<<"$PLATFORMS"; then
87+
echo "::error::$SRC is missing $required; refusing to promote a partial image"
88+
exit 1
89+
fi
90+
done
91+
92+
- name: Point :latest at ${{ inputs.version }}
93+
env:
94+
VERSION: ${{ inputs.version }}
95+
run: |
96+
set -euo pipefail
97+
docker buildx imagetools create \
98+
--tag "${IMAGE_NAME}:latest" \
99+
"${IMAGE_NAME}:${VERSION}"
100+
docker buildx imagetools inspect "${IMAGE_NAME}:latest"
101+
102+
- name: Summary
103+
env:
104+
VERSION: ${{ inputs.version }}
105+
run: |
106+
{
107+
echo "### \`${IMAGE_NAME}:latest\` now points at \`${VERSION}\`"
108+
echo
109+
echo 'Copied by digest, so :latest is identical to the verified image.'
110+
} >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)