-
Notifications
You must be signed in to change notification settings - Fork 1
205 lines (182 loc) · 7.42 KB
/
Copy pathpublish-image.yml
File metadata and controls
205 lines (182 loc) · 7.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
name: Build and Publish Container Image
# Publishes ghcr.io/esipfed/mc2 from main (and version tags) as a MULTI-ARCH
# image (linux/amd64 + linux/arm64), so `docker pull` Just Works on Intel and
# Apple-Silicon machines alike — no `platform:` override needed in compose.
#
# Shape: a matrix job builds each architecture NATIVELY (ubuntu-latest for
# amd64, ubuntu-24.04-arm for arm64 — no QEMU emulation), runs the MCP
# conformance gates INSIDE that arch's image, then pushes it by digest.
# A small merge job stitches the per-arch digests into one multi-arch
# manifest and applies the human-readable tags. PRs stop at the gates
# (nothing is pushed).
#
# Auth: the workflow-scoped GITHUB_TOKEN with `packages: write` — no PAT.
# The first successful push auto-creates the GHCR package, links it to this
# repository, and grants this repo's Actions write access. New GHCR packages
# default to PRIVATE; a maintainer flips visibility to public once in the
# package settings (Package settings → Danger Zone → Change visibility).
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
workflow_dispatch:
env:
IMAGE_NAME: ghcr.io/esipfed/mc2
jobs:
# ---------- Per-arch: build natively, gate, push by digest ---------- #
build-and-gate:
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- name: Checkout source
uses: actions/checkout@v4
# Sanitized platform string (linux/amd64 -> linux-amd64) for cache
# scopes and artifact names, which don't allow slashes.
- name: Prepare platform slug
run: |
platform="${{ matrix.platform }}"
echo "PLATFORM_SLUG=${platform//\//-}" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# OCI labels only here — the human-readable tags are applied by the
# merge job on the finished multi-arch manifest.
- name: Compute image metadata (labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
# Build first and `load` into the runner's daemon so the conformance
# gates below run against the EXACT artifact we're about to ship
# (same layers, same digest — the digest push below is a pure cache
# hit, not a rebuild).
- name: Build image (load for the conformance gates)
uses: docker/build-push-action@v6
with:
context: .
push: false
load: true
platforms: ${{ matrix.platform }}
tags: ${{ env.IMAGE_NAME }}:gate
cache-from: type=gha,scope=${{ env.PLATFORM_SLUG }}
cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_SLUG }}
# ---------- MCP conformance + auth gates ---------- #
# Run the acceptance suites INSIDE the freshly built image — against the
# deployable artifact with the real runtime env (GDAL, mcp SDK, pyjwt),
# once per architecture, on native hardware. Each suite runs in its own
# container so the Streamable-HTTP session manager's once-per-process
# run() never collides. A failure here fails the job and BLOCKS the
# push step below.
- name: MCP conformance gate (tools/protocol/errors/root-path)
run: |
timeout 120 docker run --rm --workdir /app/server \
${{ env.IMAGE_NAME }}:gate \
python tests/test_mcp.py
- name: MCP authorization gate (§5b Resource Server)
run: |
timeout 120 docker run --rm --workdir /app/server \
${{ env.IMAGE_NAME }}:gate \
python tests/test_mcp_auth.py
- name: MCP standalone auth portal gate
run: |
timeout 120 docker run --rm --workdir /app/server \
${{ env.IMAGE_NAME }}:gate \
python tests/test_portal.py
# ---------- Push by digest (main + tags only; PRs stop above) ---------- #
# push-by-digest uploads the arch image WITHOUT a tag; the merge job
# assembles the digests into the tagged multi-arch manifest.
- name: Push image by digest
id: push
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ env.PLATFORM_SLUG }}
- name: Export digest
if: github.event_name != 'pull_request'
run: |
mkdir -p "${{ runner.temp }}/digests"
digest="${{ steps.push.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_SLUG }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
# ---------- Merge per-arch digests into one multi-arch manifest ---------- #
merge-manifest:
if: github.event_name != 'pull_request'
needs: build-and-gate
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Tag scheme: latest on main, vX.Y.Z + X.Y on version tags,
# sha-<short> always (immutable pin for deployments).
- name: Compute image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-
- name: Create multi-arch manifest and push tags
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect + summary
run: |
docker buildx imagetools inspect "${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}"
{
echo "### Published :package: (multi-arch: linux/amd64 + linux/arm64)"
echo ""
echo '```'
echo "${{ steps.meta.outputs.tags }}"
echo '```'
echo ""
echo "Pull with: \`docker pull ${{ env.IMAGE_NAME }}:latest\`"
} >> "$GITHUB_STEP_SUMMARY"