mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-23 10:00:50 +00:00
## Summary Adds `.github/workflows/release-fast-path.yml`: a metadata-only re-tag workflow that fires on `push.tags: v[0-9]+.[0-9]+.[0-9]+` and, when `:edge`'s `org.opencontainers.image.revision` label matches the tag SHA, applies `:vX.Y.Z`, `:vX.Y`, `:vX`, `:latest` to the existing edge manifest via `crane tag`. No rebuild, no test re-run — ~seconds vs ~30 min today. If the SHA doesn't match (tag points to an older commit, or `:edge` wasn't built yet), it dispatches the existing `deploy.yml` pipeline as a fallback so validated bytes always ship. To prevent double-fire, `deploy.yml`'s top-level `on:` block drops `tags: ['v*']` — `release-fast-path.yml` is now the sole consumer of `push.tags`. Edge publishing on master push is untouched. ## TDD Red commit adds `cmd/server/release_fast_path_workflow_test.go` (two tests: one asserts the new workflow exists with the required trigger/permissions/markers; the other asserts `deploy.yml`'s `on:` block no longer mentions `tags:`). Both fail on assertions in the red commit. Green commit adds the workflow file + edits `deploy.yml`; both pass. ## Acceptance criteria (from #1677) - Tag-CI completes in <2 min when tag SHA == `:edge` revision → fast-path is metadata-only, single short job - Falls back to full pipeline on SHA mismatch → `gh workflow run deploy.yml --ref ${{ github.ref }}` - `:vX.Y.Z` has same digest as `:edge` → `crane tag` copies the manifest, bytes are byte-identical - No regression on older-SHA tags → fallback path runs the unchanged full validation Fixes #1677 --------- Co-authored-by: Kpa-clawbot <bot@corescope.local>
This commit is contained in:
co-authored by
Kpa-clawbot
parent
626900a22a
commit
2ef7d2437d
@@ -3,7 +3,6 @@ name: CI/CD Pipeline
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
name: Release Fast-Path
|
||||
|
||||
# Issue #1677: re-tag :edge as :vX.Y.Z when the tag SHA matches :edge's
|
||||
# org.opencontainers.image.revision label. Skips ~30 min of Go test +
|
||||
# Playwright + Docker rebuild because the bytes are identical — only the
|
||||
# manifest name changes. Falls back to deploy.yml when SHAs differ so
|
||||
# tags on older commits still go through full validation.
|
||||
#
|
||||
# This workflow is the SOLE consumer of push.tags. deploy.yml's tag
|
||||
# trigger has been removed to prevent double-fire.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v[0-9]+.[0-9]+.[0-9]+']
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: release-fast-path-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
retag-or-fallback:
|
||||
name: "🏷️ Re-tag :edge → :vX.Y.Z (fast) or dispatch deploy.yml (fallback)"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install crane
|
||||
uses: imjasonh/setup-crane@v0.4
|
||||
|
||||
- name: Parse semver from tag
|
||||
id: semver
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
# Expect vMAJOR.MINOR.PATCH (workflow trigger already enforces this).
|
||||
if [[ ! "$TAG" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
echo "Tag $TAG does not match vMAJOR.MINOR.PATCH" >&2
|
||||
exit 1
|
||||
fi
|
||||
MAJOR="${BASH_REMATCH[1]}"
|
||||
MINOR="${BASH_REMATCH[2]}"
|
||||
{
|
||||
echo "tag=$TAG"
|
||||
echo "vMajor=v$MAJOR"
|
||||
echo "vMajorMinor=v$MAJOR.$MINOR"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "Parsed: $TAG → v$MAJOR / v$MAJOR.$MINOR / $TAG"
|
||||
|
||||
- name: Inspect :edge revision label
|
||||
id: edge
|
||||
run: |
|
||||
set -euo pipefail
|
||||
IMAGE="ghcr.io/kpa-clawbot/corescope"
|
||||
EDGE_REF="${IMAGE}:edge"
|
||||
# crane config returns the OCI image config JSON; the revision label
|
||||
# is set by docker/metadata-action on the master-edge build.
|
||||
# If :edge doesn't exist yet (first run on a fresh registry), fall
|
||||
# through to the slow path.
|
||||
if ! CONFIG="$(crane config "$EDGE_REF" 2>/dev/null)"; then
|
||||
echo "edge_revision=" >> "$GITHUB_OUTPUT"
|
||||
echo "no_edge=true" >> "$GITHUB_OUTPUT"
|
||||
echo ":edge not found in registry — will use fallback path"
|
||||
exit 0
|
||||
fi
|
||||
REV="$(echo "$CONFIG" | jq -r '.config.Labels["org.opencontainers.image.revision"] // ""')"
|
||||
echo "edge_revision=$REV" >> "$GITHUB_OUTPUT"
|
||||
echo "no_edge=false" >> "$GITHUB_OUTPUT"
|
||||
echo ":edge org.opencontainers.image.revision = $REV"
|
||||
echo "tag SHA (github.sha) = ${{ github.sha }}"
|
||||
|
||||
# ─────────── FAST PATH: SHAs match, metadata-only retag ───────────
|
||||
- name: Re-tag :edge → :vX.Y.Z + :vX.Y + :vX + :latest (fast path)
|
||||
if: steps.edge.outputs.no_edge == 'false' && steps.edge.outputs.edge_revision == github.sha
|
||||
run: |
|
||||
set -euo pipefail
|
||||
IMAGE="ghcr.io/kpa-clawbot/corescope"
|
||||
SRC="${IMAGE}:edge"
|
||||
echo "SHA match — fast-path re-tag from $SRC"
|
||||
for NEW_TAG in \
|
||||
"${{ steps.semver.outputs.tag }}" \
|
||||
"${{ steps.semver.outputs.vMajorMinor }}" \
|
||||
"${{ steps.semver.outputs.vMajor }}" \
|
||||
"latest"; do
|
||||
echo " crane tag $SRC $NEW_TAG"
|
||||
crane tag "$SRC" "$NEW_TAG"
|
||||
done
|
||||
echo "Fast-path complete — all tags point at the :edge manifest digest."
|
||||
|
||||
# ─────────── FALLBACK: SHAs differ, run the full pipeline ───────────
|
||||
- name: Dispatch full deploy.yml pipeline (fallback)
|
||||
if: steps.edge.outputs.no_edge == 'true' || steps.edge.outputs.edge_revision != github.sha
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "SHA mismatch (or no :edge) — falling back to full pipeline"
|
||||
echo " :edge revision = '${{ steps.edge.outputs.edge_revision }}'"
|
||||
echo " tag SHA = '${{ github.sha }}'"
|
||||
gh workflow run deploy.yml \
|
||||
--repo "${{ github.repository }}" \
|
||||
--ref "${{ github.ref }}"
|
||||
echo "Dispatched deploy.yml against ${{ github.ref }}"
|
||||
@@ -0,0 +1,82 @@
|
||||
// Tests for issue #1677: release fast-path workflow.
|
||||
//
|
||||
// These tests gate the workflow config (not Go code) by parsing the YAML
|
||||
// files as text and asserting structural invariants. They follow the same
|
||||
// "config gate" pattern as openapi_completeness_test.go.
|
||||
//
|
||||
// 1. .github/workflows/release-fast-path.yml MUST exist and own the
|
||||
// push.tags trigger for v-tags, with the two execution branches
|
||||
// (re-tag-via-crane on SHA match, fallback to deploy.yml otherwise).
|
||||
// 2. .github/workflows/deploy.yml MUST NOT trigger on push.tags any
|
||||
// more — the fast-path workflow owns tag pushes to avoid double-fire.
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
fastPathWorkflowRel = "../../.github/workflows/release-fast-path.yml"
|
||||
deployWorkflowRel = "../../.github/workflows/deploy.yml"
|
||||
)
|
||||
|
||||
func TestReleaseFastPathWorkflowExists(t *testing.T) {
|
||||
abs, _ := filepath.Abs(fastPathWorkflowRel)
|
||||
raw, err := os.ReadFile(fastPathWorkflowRel)
|
||||
if err != nil {
|
||||
t.Fatalf("issue #1677: release-fast-path.yml missing at %s: %v", abs, err)
|
||||
}
|
||||
src := string(raw)
|
||||
|
||||
// Trigger: push.tags matching semver v-tags.
|
||||
triggerRe := regexp.MustCompile(`(?m)^\s*tags:\s*\[\s*['"]v\[0-9\]\+\.\[0-9\]\+\.\[0-9\]\+['"]\s*\]`)
|
||||
if !triggerRe.MatchString(src) {
|
||||
t.Errorf("release-fast-path.yml: missing required push.tags trigger 'v[0-9]+.[0-9]+.[0-9]+'")
|
||||
}
|
||||
|
||||
// Permissions: needs packages:write to re-tag in GHCR, contents:read for checkout.
|
||||
for _, perm := range []string{"packages: write", "contents: read"} {
|
||||
if !strings.Contains(src, perm) {
|
||||
t.Errorf("release-fast-path.yml: missing required permission %q", perm)
|
||||
}
|
||||
}
|
||||
|
||||
// Required markers covering both execution branches:
|
||||
// - re-tag path: install crane, read :edge revision label, apply new tags
|
||||
// - fallback path: dispatch the existing deploy.yml pipeline
|
||||
required := []string{
|
||||
"imjasonh/setup-crane", // crane install action
|
||||
"org.opencontainers.image.revision", // label inspected on :edge
|
||||
"ghcr.io/kpa-clawbot/corescope", // image ref
|
||||
":edge", // source tag we copy from
|
||||
"crane tag", // metadata-only retag
|
||||
"workflow run deploy.yml", // fallback dispatch
|
||||
}
|
||||
for _, need := range required {
|
||||
if !strings.Contains(src, need) {
|
||||
t.Errorf("release-fast-path.yml: missing required marker %q (issue #1677 fix-path)", need)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployWorkflowNoLongerTriggersOnTags(t *testing.T) {
|
||||
raw, err := os.ReadFile(deployWorkflowRel)
|
||||
if err != nil {
|
||||
t.Fatalf("deploy.yml: %v", err)
|
||||
}
|
||||
// Extract the top-level `on:` block: from `^on:` up to the next
|
||||
// top-level YAML key (line that starts in column 0 with a letter).
|
||||
blockRe := regexp.MustCompile(`(?ms)^on:\s*\n(.*?)\n([a-zA-Z][a-zA-Z0-9_-]*:)`)
|
||||
m := blockRe.FindStringSubmatch(string(raw))
|
||||
if m == nil {
|
||||
t.Fatalf("deploy.yml: could not locate top-level on: block")
|
||||
}
|
||||
onBlock := m[1]
|
||||
if regexp.MustCompile(`(?m)^\s*tags:\s*\[`).MatchString(onBlock) {
|
||||
t.Errorf("deploy.yml: on: block still triggers on push.tags; the fast-path workflow (release-fast-path.yml) must own tag pushes to avoid double-fire (issue #1677).\non-block was:\n%s", onBlock)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user