Repository navigation
184 lines (177 loc) · 8.29 KB
/
Copy pathrelease.yml
File metadata and controls
184 lines (177 loc) · 8.29 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
name: release
# Images on tag push, and only then: `main` is a development line, a tag is a
# statement that the three gates were green together. Both architectures,
# because the reference deployment itself runs on arm64.
on:
push:
tags: ["v*"]
# By hand, for a version whose tag already exists: the day the registry
# refused a push for reasons that were not the code's (2026-09-15, a package
# still owned by the repository this one replaced), re-running the tag's
# workflow would have carried the same fault. This builds from the chosen
# ref and stamps the version given; the tag stays where it is.
workflow_dispatch:
inputs:
version:
description: "Release version to publish, with or without a leading v"
required: true
type: string
permissions:
contents: read
packages: write
# Grouped but NOT cancel-in-progress: this is the one workflow that publishes.
# Cancelling a run mid-flight leaves whichever services already finished with a
# moved `:latest` and the rest pointing at the previous release — a split stack
# nobody asked for and nothing records. Two tags pushed close together queue and
# both finish, in order, which is the behaviour a registry wants.
concurrency:
group: release-${{ github.ref }}
jobs:
# The header's claim, made true. Nothing used to verify it: tags are cut by
# hand, the per-service CI workflows are path-filtered so they may never have
# run on the tagged commit at all, and a tag on a red commit published
# `:latest` for all three services without a word. The gate was a sentence in
# a comment, not a job.
#
# It delegates to each component's own scripts/gate.sh instead of repeating
# the step lists. Those scripts ARE the CI replicas — each component's
# test_gate_matches_ci pins its gate and its workflow to each other — and a
# third copy of the list here would only be a third thing to drift.
gate:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
# Each service gates on the interpreter its own image ships, which is
# why this carries a version per service rather than one for all three:
# a gate that passes on a Python nobody deploys has proved less than it
# looks like. Keep each entry equal to that service's Dockerfile FROM;
# scripts/assert_python_pins.py reads both files and fails on the
# drift. hookprobe's entry said 3.12 while its image was 3.14.
include:
- service: hookrelay
python: "3.15"
- service: hookjudge
python: "3.15"
- service: hookprobe
python: "3.15"
defaults:
run:
working-directory: ${{ matrix.service }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python }}
cache: "pip"
- run: python -m pip install --upgrade pip
# Every tool the gate runs is pinned in the component's requirements.txt,
# ruff included — so this install is the whole toolchain, at the versions
# the component's own CI job uses.
- run: pip install -r requirements.txt
# PY has to be a path: the gate tests it with -x before running, and a
# bare `python` would look like a missing venv and abort with setup advice.
# Node is pre-installed on GitHub-hosted ubuntu runners, which is what the
# gate's inline-page-JS step needs.
- run: PY="$(command -v python)" bash scripts/gate.sh
# The three checks no component gate covers — the docs links, the shared
# design language, the agent notes. ci-stack.yml runs them on every push, but
# "green on main last week" is not "green on this commit", and a tag is cut
# from a commit.
stack:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
- name: every relative link in the docs resolves
run: python3 scripts/check-docs.py
- name: the three pages share one design language
run: python3 scripts/assert_design.py
- name: agent notes keep their shape
run: python3 scripts/assert_agent_notes.py
images:
needs: [gate, stack]
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
# One package refusing a push must not cancel the other three: each
# image is its own promise, and the failure that taught this was a
# registry permission, not a build.
fail-fast: false
matrix:
# The fourth image is the chat bridge (docs/bridge-protocol.md): the
# sidecar that renders the pipe's card model for Feishu, as an app or
# through a custom bot's webhook. It lives under deploy/, so the build
# context is named per entry rather than assumed to be the service name.
include:
- { service: hookrelay, context: hookrelay }
- { service: hookjudge, context: hookjudge }
- { service: hookprobe, context: hookprobe }
- { service: lark-bridge, context: deploy/lark-bridge }
steps:
- uses: actions/checkout@v7
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
# GHCR_TOKEN, when set, is a personal token of the package owner with
# write:packages: the registry decides by package OWNERSHIP, and these
# four packages were created by the repository this one replaced, so
# the job token can be refused however the workflow's permissions read.
password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }}
- id: version
run: |
raw="${{ inputs.version || github.ref_name }}"
echo "version=${raw#v}" >> "$GITHUB_OUTPUT"
# Only the immutable version tag here. `:latest` is moved by the promote
# job below, once all four have succeeded — see the comment there.
- uses: docker/build-push-action@v7
with:
context: ${{ matrix.context }}
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/${{ github.repository_owner }}/${{ matrix.service }}:${{ steps.version.outputs.version }}
cache-from: type=gha,scope=${{ matrix.service }}
cache-to: type=gha,scope=${{ matrix.service }},mode=max
# `:latest` moves for all four or for none of them.
#
# The matrix above publishes per service, so a failure in the third leg used to
# leave the first two already pointing `:latest` at the new release and the
# third at the old one — a split stack, and nothing anywhere recording that it
# is split. The three components speak one wire contract to each other; half of
# them being a version ahead is exactly the shape of problem that takes a day
# to see. The version tags are immutable and land per leg, so a partial run
# leaves them harmlessly published and nobody's `:latest` moved. The bridge is
# in the same set: a pipe that speaks a newer protocol than the bridge it
# ships beside is the same split, one hop further out.
#
# imagetools re-tags the existing multi-arch manifest — no rebuild, no pull,
# and the digest `:latest` gains is the same one the version tag already has.
promote:
needs: images
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
# GHCR_TOKEN, when set, is a personal token of the package owner with
# write:packages: the registry decides by package OWNERSHIP, and these
# four packages were created by the repository this one replaced, so
# the job token can be refused however the workflow's permissions read.
password: ${{ secrets.GHCR_TOKEN || secrets.GITHUB_TOKEN }}
- id: version
run: |
raw="${{ inputs.version || github.ref_name }}"
echo "version=${raw#v}" >> "$GITHUB_OUTPUT"
- name: move :latest to this release
run: |
for service in hookrelay hookjudge hookprobe lark-bridge; do
repo="ghcr.io/${{ github.repository_owner }}/$service"
docker buildx imagetools create \
--tag "$repo:latest" "$repo:${{ steps.version.outputs.version }}"
done