feat: make release chain traceable by sha
This commit is contained in:
parent
16a4d430a4
commit
7054ecae5e
21
.github/scripts/utils/preferred-image-ref.sh
vendored
Normal file
21
.github/scripts/utils/preferred-image-ref.sh
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
tags="$1"
|
||||
preferred=""
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" == *":latest" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ -n "$line" ]]; then
|
||||
preferred="$line"
|
||||
break
|
||||
fi
|
||||
done <<< "$tags"
|
||||
|
||||
if [[ -z "$preferred" ]]; then
|
||||
preferred="$(echo "$tags" | head -n 1)"
|
||||
fi
|
||||
|
||||
echo "$preferred"
|
||||
24
.github/workflows/pipeline.yml
vendored
24
.github/workflows/pipeline.yml
vendored
@ -46,7 +46,6 @@ jobs:
|
||||
outputs:
|
||||
target_host: ${{ steps.flags.outputs.target_host }}
|
||||
run_apply: ${{ steps.flags.outputs.run_apply }}
|
||||
image_tag: ${{ steps.flags.outputs.image_tag }}
|
||||
push_image: ${{ steps.flags.outputs.push_image }}
|
||||
push_latest: ${{ steps.flags.outputs.push_latest }}
|
||||
steps:
|
||||
@ -62,7 +61,6 @@ jobs:
|
||||
DEFAULT_TARGET_HOST: ${{ env.DEFAULT_TARGET_HOST }}
|
||||
INPUT_TARGET_HOST: ${{ inputs.target_host }}
|
||||
INPUT_RUN_APPLY: ${{ inputs.run_apply }}
|
||||
INPUT_IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
INPUT_PUSH_IMAGE: ${{ inputs.push_image }}
|
||||
INPUT_PUSH_LATEST: ${{ inputs.push_latest }}
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
@ -81,7 +79,7 @@ jobs:
|
||||
SERVICE_IMAGE_NAME: accounts
|
||||
outputs:
|
||||
service_image_repo: ${{ steps.service_image.outputs.repo }}
|
||||
service_preferred_tag: ${{ steps.service_preferred.outputs.tag }}
|
||||
service_image_ref: ${{ steps.service_ref.outputs.image_ref }}
|
||||
steps:
|
||||
- name: Check Out Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@ -110,16 +108,15 @@ jobs:
|
||||
with:
|
||||
images: ${{ steps.service_image.outputs.repo }}
|
||||
tags: |
|
||||
type=raw,value=${{ needs.prep.outputs.image_tag }},enable=${{ needs.prep.outputs.image_tag != '' }}
|
||||
type=sha,format=short,enable=${{ needs.prep.outputs.image_tag == '' }}
|
||||
type=sha,format=long
|
||||
type=raw,value=latest,enable=${{ needs.prep.outputs.push_latest == 'true' || github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Resolve Service Preferred Tag
|
||||
id: service_preferred
|
||||
- name: Resolve Service Image Ref
|
||||
id: service_ref
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="$(bash .github/scripts/utils/preferred-tag.sh "${{ steps.service_meta.outputs.tags }}")"
|
||||
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
||||
image_ref="$(bash .github/scripts/utils/preferred-image-ref.sh "${{ steps.service_meta.outputs.tags }}")"
|
||||
echo "image_ref=${image_ref}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build And Push Service Image
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
@ -140,7 +137,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
image: ${{ needs.build.outputs.service_image_repo }}
|
||||
preferred_tag: ${{ needs.build.outputs.service_preferred_tag }}
|
||||
image_ref: ${{ needs.build.outputs.service_image_ref }}
|
||||
run_apply: ${{ needs.prep.outputs.run_apply }}
|
||||
pushed: "true"
|
||||
steps:
|
||||
@ -159,8 +156,8 @@ jobs:
|
||||
id: deploy_image_tag
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="${{ needs.build.outputs.service_preferred_tag }}"
|
||||
echo "value=${tag##*:}" >> "$GITHUB_OUTPUT"
|
||||
image_ref="${{ needs.build.outputs.service_image_ref }}"
|
||||
echo "value=${image_ref##*:}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set Up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.0.0
|
||||
@ -209,6 +206,7 @@ jobs:
|
||||
validate:
|
||||
name: Validate
|
||||
needs:
|
||||
- build
|
||||
- deploy
|
||||
if: ${{ always() && needs.deploy.result == 'success' && needs.deploy.outputs.pushed == 'true' && needs.deploy.outputs.run_apply == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
@ -217,4 +215,4 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Deployed Accounts Service
|
||||
run: bash ./scripts/github-actions/validate-deploy.sh https://accounts.svc.plus
|
||||
run: bash ./scripts/github-actions/validate-deploy.sh "${{ needs.build.outputs.service_image_ref }}" https://accounts.svc.plus
|
||||
|
||||
@ -5,6 +5,7 @@ service_compose_container_port: 8080
|
||||
service_compose_git_short_commit: "{{ lookup('ansible.builtin.env', 'GIT_SHORT_COMMIT') | default('manual', true) }}"
|
||||
service_compose_env_common:
|
||||
CONFIG_TEMPLATE: /app/config/account.cloudrun.yaml
|
||||
IMAGE: "{{ service_compose_image }}"
|
||||
service_compose_deploy_targets:
|
||||
- name: prod
|
||||
deploy_subdomain_prefix: accounts
|
||||
@ -18,4 +19,3 @@ service_compose_deploy_targets:
|
||||
- accounts-preview.svc.plus
|
||||
host_port: 18081
|
||||
env: {}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -15,7 +16,7 @@ import (
|
||||
"account/internal/store"
|
||||
)
|
||||
|
||||
func TestAgentUsersUseAccountUUIDAsStatsEmail(t *testing.T) {
|
||||
func TestAgentUsersUseAccountEmailAsStatsEmail(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
st := store.NewMemoryStore()
|
||||
@ -74,12 +75,12 @@ func TestAgentUsersUseAccountUUIDAsStatsEmail(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, client := range payload.Clients {
|
||||
if client.Email == user.ID {
|
||||
if client.Email == strings.ToLower(strings.TrimSpace(user.Email)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("expected stats email %q in payload, got %#v", user.ID, payload.Clients)
|
||||
t.Fatalf("expected stats email %q in payload, got %#v", user.Email, payload.Clients)
|
||||
}
|
||||
|
||||
func TestAccountUsageAndPolicyEndpoints(t *testing.T) {
|
||||
|
||||
@ -76,7 +76,7 @@ func (h *handler) listAgentUsers(c *gin.Context) {
|
||||
if id != "" {
|
||||
clients = append(clients, xrayconfig.Client{
|
||||
ID: id,
|
||||
Email: strings.TrimSpace(sandboxUser.ID),
|
||||
Email: strings.ToLower(strings.TrimSpace(sandboxUser.Email)),
|
||||
Flow: xrayconfig.DefaultFlow,
|
||||
})
|
||||
}
|
||||
@ -92,7 +92,7 @@ func (h *handler) listAgentUsers(c *gin.Context) {
|
||||
}
|
||||
clients = append(clients, xrayconfig.Client{
|
||||
ID: id,
|
||||
Email: strings.TrimSpace(u.ID),
|
||||
Email: strings.ToLower(strings.TrimSpace(u.Email)),
|
||||
Flow: xrayconfig.DefaultFlow,
|
||||
})
|
||||
}
|
||||
|
||||
65
api/api.go
65
api/api.go
@ -13,6 +13,7 @@ import (
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -44,6 +45,13 @@ const defaultBridgeBootstrapTarget = "https://xworkmate-bridge.svc.plus"
|
||||
|
||||
const sessionCookieName = "xc_session"
|
||||
|
||||
type imageVersionInfo struct {
|
||||
ImageRef string `json:"image_ref"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type session struct {
|
||||
userID string
|
||||
expiresAt time.Time
|
||||
@ -326,6 +334,17 @@ func RegisterRoutes(r *gin.Engine, opts ...Option) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
r.GET("/api/ping", func(c *gin.Context) {
|
||||
info := parseImageVersionInfo(os.Getenv("IMAGE"))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"image": info.ImageRef,
|
||||
"tag": info.Tag,
|
||||
"commit": info.Commit,
|
||||
"version": info.Version,
|
||||
})
|
||||
})
|
||||
|
||||
authGroup := r.Group("/api/auth")
|
||||
|
||||
authGroup.POST("/register", h.register)
|
||||
@ -3039,6 +3058,52 @@ func (h *handler) isRootAccount(user *store.User) bool {
|
||||
return store.IsRootRole(user.Role) && strings.EqualFold(strings.TrimSpace(user.Email), store.RootAdminEmail)
|
||||
}
|
||||
|
||||
func parseImageVersionInfo(imageRef string) imageVersionInfo {
|
||||
ref := strings.TrimSpace(imageRef)
|
||||
info := imageVersionInfo{ImageRef: ref}
|
||||
if ref == "" {
|
||||
return info
|
||||
}
|
||||
|
||||
if idx := strings.LastIndex(ref, "@"); idx >= 0 {
|
||||
ref = ref[:idx]
|
||||
}
|
||||
|
||||
tag := ref
|
||||
if idx := strings.LastIndex(tag, ":"); idx >= 0 && idx > strings.LastIndex(tag, "/") {
|
||||
tag = tag[idx+1:]
|
||||
}
|
||||
tag = strings.TrimSpace(tag)
|
||||
info.Tag = tag
|
||||
|
||||
switch {
|
||||
case isHexCommit(tag):
|
||||
info.Commit = tag
|
||||
info.Version = tag
|
||||
case strings.HasPrefix(tag, "v") && len(tag) > 1:
|
||||
info.Version = tag
|
||||
default:
|
||||
info.Version = tag
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func isHexCommit(value string) bool {
|
||||
if len(value) < 7 || len(value) > 40 {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
switch {
|
||||
case r >= '0' && r <= '9':
|
||||
case r >= 'a' && r <= 'f':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func respondError(c *gin.Context, status int, code, message string) {
|
||||
if status >= 500 {
|
||||
slog.Error("api_error", "status", status, "code", code, "message", message, "path", c.Request.URL.Path, "method", c.Request.Method)
|
||||
|
||||
@ -318,10 +318,10 @@ func TestAgentServerUsers_DefaultSyncIncludesSandboxAndRegularUsers(t *testing.T
|
||||
seenSandbox := false
|
||||
seenNormal := false
|
||||
for _, c := range payload.Clients {
|
||||
if c.Email == sandbox.ID && strings.TrimSpace(c.ID) != "" {
|
||||
if c.Email == strings.ToLower(strings.TrimSpace(sandbox.Email)) && strings.TrimSpace(c.ID) != "" {
|
||||
seenSandbox = true
|
||||
}
|
||||
if c.Email == normal.ID && strings.TrimSpace(c.ID) != "" {
|
||||
if c.Email == strings.ToLower(strings.TrimSpace(normal.Email)) && strings.TrimSpace(c.ID) != "" {
|
||||
seenNormal = true
|
||||
}
|
||||
}
|
||||
@ -1494,6 +1494,39 @@ func TestHealthzEndpoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPingEndpointDerivesVersionFromImageEnv(t *testing.T) {
|
||||
t.Setenv("IMAGE", "ghcr.io/example/accounts:abcdef1234567890abcdef1234567890abcdef12")
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
router := gin.New()
|
||||
RegisterRoutes(router)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected ping endpoint to return 200, got %d", rr.Code)
|
||||
}
|
||||
|
||||
var resp map[string]string
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to decode ping response: %v", err)
|
||||
}
|
||||
if got := resp["image"]; got != "ghcr.io/example/accounts:abcdef1234567890abcdef1234567890abcdef12" {
|
||||
t.Fatalf("expected image ref from env, got %q", got)
|
||||
}
|
||||
if got := resp["tag"]; got != "abcdef1234567890abcdef1234567890abcdef12" {
|
||||
t.Fatalf("expected tag derived from image ref, got %q", got)
|
||||
}
|
||||
if got := resp["commit"]; got != "abcdef1234567890abcdef1234567890abcdef12" {
|
||||
t.Fatalf("expected commit derived from image ref, got %q", got)
|
||||
}
|
||||
if got := resp["version"]; got != "abcdef1234567890abcdef1234567890abcdef12" {
|
||||
t.Fatalf("expected version derived from image ref, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordResetFlow(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@ fi
|
||||
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
|
||||
TARGET_HOST="${INPUT_TARGET_HOST:-${TARGET_HOST}}"
|
||||
[[ "${INPUT_RUN_APPLY:-true}" == "true" ]] && RUN_APPLY=true || RUN_APPLY=false
|
||||
IMAGE_TAG="${INPUT_IMAGE_TAG:-}"
|
||||
[[ "${INPUT_PUSH_IMAGE:-true}" == "true" ]] && PUSH_IMAGE=true || PUSH_IMAGE=false
|
||||
[[ "${INPUT_PUSH_LATEST:-false}" == "true" ]] && PUSH_LATEST=true || PUSH_LATEST=false
|
||||
[[ "${INPUT_RUN_BASE_IMAGES:-false}" == "true" ]] && RUN_BASE_IMAGES=true || RUN_BASE_IMAGES=false
|
||||
|
||||
@ -1,12 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-https://accounts.svc.plus}"
|
||||
IMAGE_REF="${1:?image_ref is required}"
|
||||
BASE_URL="${2:-https://accounts.svc.plus}"
|
||||
|
||||
curl \
|
||||
--silent \
|
||||
--show-error \
|
||||
--fail \
|
||||
--location \
|
||||
--max-time 20 \
|
||||
"${BASE_URL}/healthz" | grep -q '"status":"ok"'
|
||||
image_ref="$(printf '%s' "${IMAGE_REF}" | tr -d '\n' | xargs)"
|
||||
if [[ -z "${image_ref}" ]]; then
|
||||
echo "image_ref is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
image_no_digest="${image_ref%@*}"
|
||||
tag="${image_no_digest##*:}"
|
||||
if [[ "${image_no_digest}" == "${tag}" ]]; then
|
||||
tag=""
|
||||
fi
|
||||
|
||||
commit=""
|
||||
version="${tag}"
|
||||
|
||||
if [[ "${tag}" =~ ^[0-9a-f]{7,40}$ ]]; then
|
||||
commit="${tag}"
|
||||
fi
|
||||
|
||||
ping_json="$(
|
||||
curl \
|
||||
--silent \
|
||||
--show-error \
|
||||
--fail \
|
||||
--location \
|
||||
--max-time 20 \
|
||||
"${BASE_URL}/api/ping"
|
||||
)"
|
||||
|
||||
PING_JSON="${ping_json}" python3 - "${image_ref}" "${tag}" "${commit}" "${version}" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
image_ref, tag, commit, version = sys.argv[1:5]
|
||||
payload = json.loads(os.environ["PING_JSON"])
|
||||
|
||||
if payload.get("status") != "ok":
|
||||
raise SystemExit("ping status not ok")
|
||||
|
||||
if payload.get("image") != image_ref:
|
||||
raise SystemExit(f"expected image {image_ref!r}, got {payload.get('image')!r}")
|
||||
|
||||
if tag and payload.get("tag") != tag:
|
||||
raise SystemExit(f"expected tag {tag!r}, got {payload.get('tag')!r}")
|
||||
|
||||
if commit and payload.get("commit") != commit:
|
||||
raise SystemExit(f"expected commit {commit!r}, got {payload.get('commit')!r}")
|
||||
|
||||
if version and payload.get("version") != version:
|
||||
raise SystemExit(f"expected version {version!r}, got {payload.get('version')!r}")
|
||||
PY
|
||||
|
||||
Loading…
Reference in New Issue
Block a user