feat: enforce traceable release chain
This commit is contained in:
parent
a757cfcb23
commit
6e560c61e3
51
.github/workflows/pipeline.yml
vendored
51
.github/workflows/pipeline.yml
vendored
@ -79,6 +79,8 @@ jobs:
|
|||||||
SERVICE_IMAGE_NAME: accounts
|
SERVICE_IMAGE_NAME: accounts
|
||||||
outputs:
|
outputs:
|
||||||
service_image_repo: ${{ steps.service_image.outputs.repo }}
|
service_image_repo: ${{ steps.service_image.outputs.repo }}
|
||||||
|
service_image_tag: ${{ steps.service_ref.outputs.image_tag }}
|
||||||
|
service_image_commit: ${{ steps.service_ref.outputs.image_commit }}
|
||||||
service_image_ref: ${{ steps.service_ref.outputs.image_ref }}
|
service_image_ref: ${{ steps.service_ref.outputs.image_ref }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check Out Repository
|
- name: Check Out Repository
|
||||||
@ -116,6 +118,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
image_ref="$(bash .github/scripts/utils/preferred-image-ref.sh "${{ steps.service_meta.outputs.tags }}")"
|
image_ref="$(bash .github/scripts/utils/preferred-image-ref.sh "${{ steps.service_meta.outputs.tags }}")"
|
||||||
|
image_no_digest="${image_ref%@*}"
|
||||||
|
image_tag="${image_no_digest##*:}"
|
||||||
|
image_commit=""
|
||||||
|
if [[ "${image_tag}" =~ ^[0-9a-f]{7,40}$ ]]; then
|
||||||
|
image_commit="${image_tag}"
|
||||||
|
fi
|
||||||
|
echo "image_tag=${image_tag}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "image_commit=${image_commit}" >> "$GITHUB_OUTPUT"
|
||||||
echo "image_ref=${image_ref}" >> "$GITHUB_OUTPUT"
|
echo "image_ref=${image_ref}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Build And Push Service Image
|
- name: Build And Push Service Image
|
||||||
@ -136,7 +146,7 @@ jobs:
|
|||||||
if: ${{ needs.prep.outputs.push_image == 'true' }}
|
if: ${{ needs.prep.outputs.push_image == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
image: ${{ needs.build.outputs.service_image_repo }}
|
image: ${{ needs.build.outputs.service_image_ref }}
|
||||||
image_ref: ${{ needs.build.outputs.service_image_ref }}
|
image_ref: ${{ needs.build.outputs.service_image_ref }}
|
||||||
run_apply: ${{ needs.prep.outputs.run_apply }}
|
run_apply: ${{ needs.prep.outputs.run_apply }}
|
||||||
pushed: "true"
|
pushed: "true"
|
||||||
@ -144,6 +154,25 @@ jobs:
|
|||||||
- name: Check Out Repository
|
- name: Check Out Repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Resolve Deploy Image Artifact
|
||||||
|
id: deploy_image
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
image_ref="${{ needs.build.outputs.service_image_ref }}"
|
||||||
|
image_no_digest="${image_ref%@*}"
|
||||||
|
image_repo="${image_no_digest%:*}"
|
||||||
|
image_tag="${image_no_digest##*:}"
|
||||||
|
|
||||||
|
if [[ -z "${image_ref}" || -z "${image_repo}" || -z "${image_tag}" || "${image_repo}" == "${image_tag}" ]]; then
|
||||||
|
echo "invalid deploy image artifact: ${image_ref}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "image_ref=${image_ref}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "image_repo=${image_repo}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "image_tag=${image_tag}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Check Out Playbooks Repository
|
- name: Check Out Playbooks Repository
|
||||||
# Pull latest playbooks HEAD from the default branch.
|
# Pull latest playbooks HEAD from the default branch.
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
@ -152,12 +181,19 @@ jobs:
|
|||||||
token: ${{ secrets.WORKSPACE_REPO_TOKEN || github.token }}
|
token: ${{ secrets.WORKSPACE_REPO_TOKEN || github.token }}
|
||||||
path: playbooks
|
path: playbooks
|
||||||
|
|
||||||
- name: Resolve Deploy Image Tag
|
- name: Guard Against Host Image Builds
|
||||||
id: deploy_image_tag
|
working-directory: ${{ github.workspace }}/playbooks
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
image_ref="${{ needs.build.outputs.service_image_ref }}"
|
|
||||||
echo "value=${image_ref##*:}" >> "$GITHUB_OUTPUT"
|
if rg -n \
|
||||||
|
deploy_accounts_svc_plus.yml \
|
||||||
|
roles/vhosts/accounts_service \
|
||||||
|
'(docker build|podman build|docker buildx build|gcloud builds submit)' \
|
||||||
|
; then
|
||||||
|
echo "deploy flow must use the build job image artifact and must not build images on the target host" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Set Up Python
|
- name: Set Up Python
|
||||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.0.0
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.0.0
|
||||||
@ -181,8 +217,9 @@ jobs:
|
|||||||
- name: Run Accounts Deploy Playbook
|
- name: Run Accounts Deploy Playbook
|
||||||
working-directory: ${{ github.workspace }}/playbooks
|
working-directory: ${{ github.workspace }}/playbooks
|
||||||
env:
|
env:
|
||||||
ACCOUNTS_IMAGE_REPO: ${{ needs.build.outputs.service_image_repo }}
|
ACCOUNTS_IMAGE_REF: ${{ steps.deploy_image.outputs.image_ref }}
|
||||||
ACCOUNTS_IMAGE_TAG: ${{ steps.deploy_image_tag.outputs.value }}
|
ACCOUNTS_IMAGE_REPO: ${{ steps.deploy_image.outputs.image_repo }}
|
||||||
|
ACCOUNTS_IMAGE_TAG: ${{ steps.deploy_image.outputs.image_tag }}
|
||||||
ACCOUNTS_PULL_IMAGE: "true"
|
ACCOUNTS_PULL_IMAGE: "true"
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|||||||
16
AGENTS.md
Normal file
16
AGENTS.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Repository Agent Guide
|
||||||
|
|
||||||
|
Default local skill references for this repository:
|
||||||
|
|
||||||
|
- Release traceability: [skills/release-traceability/SKILL.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/accounts.svc.plus/skills/release-traceability/SKILL.md)
|
||||||
|
|
||||||
|
## Default Rule
|
||||||
|
|
||||||
|
For any change related to CI/CD, image tags, deployment contracts, release validation, or runtime version reporting, follow the release traceability skill above as the default repository policy.
|
||||||
|
|
||||||
|
## What This Means
|
||||||
|
|
||||||
|
- Treat `service_image_ref` as the single source of truth for release identity.
|
||||||
|
- Keep commit traceability tied to full `GITHUB_SHA`.
|
||||||
|
- Ensure deploy uses prebuilt images rather than building on the target host.
|
||||||
|
- Ensure `/api/ping` and validate both report and verify the same runtime image-derived version data.
|
||||||
@ -3080,6 +3080,9 @@ func parseImageVersionInfo(imageRef string) imageVersionInfo {
|
|||||||
case isHexCommit(tag):
|
case isHexCommit(tag):
|
||||||
info.Commit = tag
|
info.Commit = tag
|
||||||
info.Version = tag
|
info.Version = tag
|
||||||
|
case strings.HasPrefix(tag, "sha-") && isHexCommit(strings.TrimPrefix(tag, "sha-")):
|
||||||
|
info.Commit = strings.TrimPrefix(tag, "sha-")
|
||||||
|
info.Version = tag
|
||||||
case strings.HasPrefix(tag, "v") && len(tag) > 1:
|
case strings.HasPrefix(tag, "v") && len(tag) > 1:
|
||||||
info.Version = tag
|
info.Version = tag
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -1527,6 +1527,39 @@ func TestPingEndpointDerivesVersionFromImageEnv(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPingEndpointDerivesCommitFromShaPrefixedImageTag(t *testing.T) {
|
||||||
|
t.Setenv("IMAGE", "ghcr.io/example/accounts:sha-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:sha-abcdef1234567890abcdef1234567890abcdef12" {
|
||||||
|
t.Fatalf("expected image ref from env, got %q", got)
|
||||||
|
}
|
||||||
|
if got := resp["tag"]; got != "sha-abcdef1234567890abcdef1234567890abcdef12" {
|
||||||
|
t.Fatalf("expected tag derived from image ref, got %q", got)
|
||||||
|
}
|
||||||
|
if got := resp["commit"]; got != "abcdef1234567890abcdef1234567890abcdef12" {
|
||||||
|
t.Fatalf("expected commit derived from sha-prefixed image ref, got %q", got)
|
||||||
|
}
|
||||||
|
if got := resp["version"]; got != "sha-abcdef1234567890abcdef1234567890abcdef12" {
|
||||||
|
t.Fatalf("expected version derived from image ref, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPasswordResetFlow(t *testing.T) {
|
func TestPasswordResetFlow(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,8 @@ version="${tag}"
|
|||||||
|
|
||||||
if [[ "${tag}" =~ ^[0-9a-f]{7,40}$ ]]; then
|
if [[ "${tag}" =~ ^[0-9a-f]{7,40}$ ]]; then
|
||||||
commit="${tag}"
|
commit="${tag}"
|
||||||
|
elif [[ "${tag}" =~ ^sha-([0-9a-f]{7,40})$ ]]; then
|
||||||
|
commit="${BASH_REMATCH[1]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ping_json="$(
|
ping_json="$(
|
||||||
|
|||||||
98
skills/release-traceability/SKILL.md
Normal file
98
skills/release-traceability/SKILL.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
name: release-traceability
|
||||||
|
description: Use when changing CI/CD, image tagging, deployment inputs, runtime version reporting, or validate logic for accounts.svc.plus. Enforces a single-source-of-truth release chain from GITHUB_SHA-based image build output through deploy and /api/ping validation.
|
||||||
|
license: Internal use only
|
||||||
|
metadata:
|
||||||
|
owner: cloud-neutral-toolkit
|
||||||
|
distribution: local-repo
|
||||||
|
package-format: .skill
|
||||||
|
---
|
||||||
|
|
||||||
|
# Release Traceability
|
||||||
|
|
||||||
|
Use this skill for any work that touches:
|
||||||
|
|
||||||
|
- `.github/workflows/pipeline.yml`
|
||||||
|
- image tag generation
|
||||||
|
- deploy inputs and playbook contracts
|
||||||
|
- `/api/ping` version reporting
|
||||||
|
- `scripts/github-actions/validate-deploy.sh`
|
||||||
|
- cross-repo release handoff into deployment automation
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Preserve a single, traceable release chain so every delivery can answer:
|
||||||
|
|
||||||
|
- Which commit was built?
|
||||||
|
- Which image was deployed?
|
||||||
|
- Does the running service report that exact image and commit?
|
||||||
|
|
||||||
|
## Canonical Contract
|
||||||
|
|
||||||
|
1. `GITHUB_SHA` is the only commit source for release identity.
|
||||||
|
2. Build image tags must use the full commit SHA, typically `sha-<40 hex>`.
|
||||||
|
3. `service_image_ref` is the only authoritative build artifact identifier.
|
||||||
|
4. Deploy must consume `service_image_ref` directly.
|
||||||
|
5. Any `repo` / `tag` values used during deploy must be derived from `service_image_ref`, never provided independently.
|
||||||
|
6. The running container must receive `IMAGE=<full image_ref>`.
|
||||||
|
7. `/api/ping` must derive `image`, `tag`, `commit`, and `version` from `IMAGE`.
|
||||||
|
8. Validate must accept only `image_ref` and compare remote output against values parsed from it.
|
||||||
|
|
||||||
|
## Build Rules
|
||||||
|
|
||||||
|
- Generate tags only from full `GITHUB_SHA`.
|
||||||
|
- Do not introduce manual `image_tag`, `commit_id`, `version_tag`, or similar override inputs.
|
||||||
|
- Prefer one build output:
|
||||||
|
- `service_image_ref`
|
||||||
|
- Optional outputs such as `service_image_tag` and `service_image_commit` are allowed only if parsed from `service_image_ref`.
|
||||||
|
- `latest` may exist as an auxiliary tag, but it must never be used as the authoritative release identifier.
|
||||||
|
|
||||||
|
## Deploy Rules
|
||||||
|
|
||||||
|
- Deploy must use the build job output, not a separately selected image.
|
||||||
|
- Default deploy flow must pull and run a prebuilt image.
|
||||||
|
- Do not add or preserve target-host build behavior:
|
||||||
|
- `docker build`
|
||||||
|
- `podman build`
|
||||||
|
- `docker buildx build`
|
||||||
|
- `gcloud builds submit`
|
||||||
|
- If an external repo or playbook is part of the release path, it must also accept `IMAGE_REF` / `ACCOUNTS_IMAGE_REF` and treat it as the single source of truth.
|
||||||
|
- If legacy deploy systems still need `repo` and `tag`, derive them in the deploy job from `service_image_ref`.
|
||||||
|
|
||||||
|
## Runtime Rules
|
||||||
|
|
||||||
|
- `/api/ping` must not trust a separate `COMMIT_ID` environment variable.
|
||||||
|
- `commit` must come from parsing the image tag in `IMAGE`.
|
||||||
|
- Support both:
|
||||||
|
- raw hex tags like `<40 hex>`
|
||||||
|
- SHA-prefixed tags like `sha-<40 hex>`
|
||||||
|
- If `IMAGE` is missing, return diagnostically useful empty values rather than inventing a commit.
|
||||||
|
|
||||||
|
## Validate Rules
|
||||||
|
|
||||||
|
- `scripts/github-actions/validate-deploy.sh` must accept only:
|
||||||
|
- `image_ref`
|
||||||
|
- optional base URL
|
||||||
|
- The script must parse `tag`, `commit`, and `version` from `image_ref`.
|
||||||
|
- Minimum required checks:
|
||||||
|
- remote `image == image_ref`
|
||||||
|
- remote `tag == parsed tag`
|
||||||
|
- remote `commit == parsed commit`
|
||||||
|
- remote `version == parsed version`
|
||||||
|
|
||||||
|
## Required Checks Before Completion
|
||||||
|
|
||||||
|
- Confirm the workflow still emits a full-SHA `service_image_ref`.
|
||||||
|
- Confirm deploy consumes that artifact and does not re-select the image.
|
||||||
|
- Confirm the runtime container receives `IMAGE=<service_image_ref>`.
|
||||||
|
- Confirm `/api/ping` reports values consistent with `service_image_ref`.
|
||||||
|
- Confirm validate compares against values parsed from the same `service_image_ref`.
|
||||||
|
- Confirm no target-host image build commands were introduced into the default deploy path.
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
- Passing `commit_id` separately into deploy or validate
|
||||||
|
- Letting deploy choose a different image than build produced
|
||||||
|
- Using `latest` as the release truth source
|
||||||
|
- Rebuilding images on the target host
|
||||||
|
- Returning a commit from any source other than the runtime `IMAGE`
|
||||||
Loading…
Reference in New Issue
Block a user