Skip to content

Commit 3f6bf4a

Browse files
newbe36524claude
andcommitted
feat: Add version format validation and multi-architecture verification
This change implements automatic validation for version numbers and ensures all published Docker images contain both amd64 and arm64 architectures. ## Version Format Validation - Add `IsValidVersionFormat()` method in Build.Targets.VersionMonitor.cs - Validate versions start with a digit (no 'v' prefix allowed) - Validate versions contain only letters, numbers, dots, hyphens, underscores - Skip invalid versions with warning logs in Version Monitor - Add version format validation in Docker Build workflow with error messages ## Multi-Architecture Verification - Verify all published images contain both linux/amd64 and linux/arm64 - Check Azure ACR images in "Verify Images in Registry" step - Check Aliyun ACR images in "Verify Images in Aliyun ACR" step - Fail workflow with clear error if either architecture is missing - Verify all tags: version, major.minor, major, and latest (if stable) ## Documentation - Update AGENTS.md with version format requirements and examples - Update README.md with multi-architecture build notes - Add detailed comments in workflow for validation behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8d8c9e8 commit 3f6bf4a

4 files changed

Lines changed: 217 additions & 24 deletions

File tree

.github/workflows/docker-build.yml

Lines changed: 110 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ jobs:
128128
fi
129129
echo "version=${VERSION}" >> $GITHUB_OUTPUT
130130
131+
# Version format validation (skip for 'latest')
132+
if [ "${VERSION}" != "latest" ]; then
133+
# Version must start with a digit and contain only letters, numbers, dots, hyphens, or underscores
134+
if ! [[ "${VERSION}" =~ ^[0-9][0-9A-Za-z._-]*$ ]]; then
135+
echo "::error::Invalid version format '${VERSION}'"
136+
echo "Version must start with a digit and only contain letters, numbers, dots, hyphens, or underscores."
137+
echo "Example: 1.2.3, 1.2.3-beta.1"
138+
exit 1
139+
fi
140+
echo "✓ Version format validated: ${VERSION}"
141+
fi
142+
131143
# Determine platform - default to 'all' if not specified
132144
if [ -z "${{ inputs.platform }}" ]; then
133145
# When platform is not specified or empty, default to 'all'
@@ -216,7 +228,7 @@ jobs:
216228
if: steps.config.outputs.version != 'latest'
217229
run: |
218230
VERSION="${{ steps.config.outputs.version }}"
219-
# Remove v prefix for Docker image tags
231+
# Remove v prefix for Docker image tags (for backward compatibility with tag push trigger)
220232
CLEAN_VERSION="${VERSION#v}"
221233
PLATFORM="${{ steps.config.outputs.platform }}"
222234
IMAGE_NAME="hagicode"
@@ -271,7 +283,7 @@ jobs:
271283
if: steps.config.outputs.version != 'latest' && (inputs.dry_run != 'true')
272284
run: |
273285
VERSION="${{ steps.config.outputs.version }}"
274-
# Remove v prefix for tag operations
286+
# Remove v prefix for tag operations (for backward compatibility with tag push trigger)
275287
CLEAN_VERSION="${VERSION#v}"
276288
IMAGE_NAME="hagicode"
277289
REGISTRY="${{ secrets.AZURE_ACR_REGISTRY }}"
@@ -310,8 +322,66 @@ jobs:
310322
311323
echo "Verifying images in registry..."
312324
313-
docker manifest inspect ${REGISTRY}/${IMAGE_NAME}:${VERSION} || echo "Warning: Failed to verify version tag"
314-
docker manifest inspect ${REGISTRY}/${IMAGE_NAME}:latest || echo "Warning: Failed to verify latest tag"
325+
# Parse version parts for tag verification
326+
MAJOR=$(echo ${VERSION} | cut -d. -f1)
327+
MINOR=$(echo ${VERSION} | cut -d. -f2)
328+
329+
# Multi-architecture verification requirements:
330+
# - All published images MUST contain both linux/amd64 and linux/arm64 architectures
331+
# - Verification is done for each tag: version, major.minor, major, and latest (if stable)
332+
# - If either architecture is missing, the workflow fails with a clear error message
333+
# - This ensures users on both AMD64 and ARM64 platforms can pull compatible images
334+
335+
# Verify image architectures for each tag
336+
for TAG in "${VERSION}" "${MAJOR}.${MINOR}" "${MAJOR}"; do
337+
echo "Verifying tag: ${TAG}"
338+
339+
# Extract architecture list from manifest using jq
340+
# jq is required for parsing the JSON output of docker manifest inspect
341+
ARCHITECTURES=$(docker manifest inspect ${REGISTRY}/${IMAGE_NAME}:${TAG} 2>/dev/null | jq -r '.[].Platform[].architecture' 2>/dev/null || echo "")
342+
343+
if [ -z "${ARCHITECTURES}" ]; then
344+
echo "::error::Failed to inspect manifest for tag ${TAG}"
345+
exit 1
346+
fi
347+
348+
echo "Found architectures for tag ${TAG}: ${ARCHITECTURES}"
349+
350+
# Check if amd64 is present (required for AMD64/x86_64 platforms)
351+
if ! echo "${ARCHITECTURES}" | grep -q "amd64"; then
352+
echo "::error::Tag ${TAG} does not contain amd64 architecture"
353+
exit 1
354+
fi
355+
356+
# Check if arm64 is present (required for ARM64/AArch64 platforms)
357+
if ! echo "${ARCHITECTURES}" | grep -q "arm64"; then
358+
echo "::error::Tag ${TAG} does not contain arm64 architecture"
359+
exit 1
360+
fi
361+
362+
echo "✓ Tag ${TAG} contains both amd64 and arm64 architectures"
363+
done
364+
365+
# Verify latest tag only for stable releases
366+
if [[ ! ${VERSION} =~ ^(rc|beta|alpha|preview|dev)[0-9]*$ ]] && [[ ! ${VERSION} =~ -(rc|beta|alpha|preview|dev) ]]; then
367+
echo "Verifying tag: latest"
368+
ARCHITECTURES=$(docker manifest inspect ${REGISTRY}/${IMAGE_NAME}:latest 2>/dev/null | jq -r '.[].Platform[].architecture' 2>/dev/null || echo "")
369+
370+
if [ -z "${ARCHITECTURES}" ]; then
371+
echo "::warning::Failed to inspect manifest for tag latest (may not exist)"
372+
else
373+
echo "Found architectures for tag latest: ${ARCHITECTURES}"
374+
375+
if ! echo "${ARCHITECTURES}" | grep -q "amd64" || ! echo "${ARCHITECTURES}" | grep -q "arm64"; then
376+
echo "::error::Tag latest does not contain both amd64 and arm64 architectures"
377+
exit 1
378+
fi
379+
380+
echo "✓ Tag latest contains both amd64 and arm64 architectures"
381+
fi
382+
fi
383+
384+
echo "All image tags verified successfully!"
315385
316386
- name: Post Login to Edge ACR
317387
if: steps.config.outputs.version != 'latest' && (inputs.dry_run != 'true')
@@ -334,7 +404,7 @@ jobs:
334404
if: steps.config.outputs.version != 'latest' && (inputs.dry_run != 'true')
335405
run: |
336406
VERSION="${{ steps.config.outputs.version }}"
337-
# Remove v prefix for Docker image tags
407+
# Remove v prefix for Docker image tags (for backward compatibility with tag push trigger)
338408
CLEAN_VERSION="${VERSION#v}"
339409
IMAGE_NAME="hagicode"
340410
AZURE_REGISTRY="${{ secrets.AZURE_ACR_REGISTRY }}"
@@ -408,27 +478,45 @@ jobs:
408478
409479
echo "Verifying images in Aliyun ACR..."
410480
411-
# Verify base image
412-
echo "Checking base image..."
413-
docker manifest inspect ${ALIYUN_IMAGE_NAME}:base || echo "Warning: Failed to verify base tag"
481+
# Multi-architecture verification for Aliyun ACR:
482+
# - Same requirements as Azure ACR: both amd64 and arm64 must be present
483+
# - Verified for all pushed tags: version, major.minor, major, and latest (if stable)
484+
# - Ensures Aliyun ACR images are also multi-arch compatible
414485
415-
# Verify version tag
416-
echo "Checking version tag..."
417-
docker manifest inspect ${ALIYUN_IMAGE_NAME}:${CLEAN_VERSION} || echo "Warning: Failed to verify version tag"
486+
# Define tags to verify
487+
TAGS=("${CLEAN_VERSION}" "${MAJOR}.${MINOR}" "${MAJOR}")
488+
if [ "$IS_STABLE" = "true" ]; then
489+
TAGS+=("latest")
490+
fi
418491
419-
# Verify major.minor tag
420-
echo "Checking major.minor tag..."
421-
docker manifest inspect ${ALIYUN_IMAGE_NAME}:${MAJOR}.${MINOR} || echo "Warning: Failed to verify major.minor tag"
492+
# Verify each tag's architectures
493+
for TAG in "${TAGS[@]}"; do
494+
echo "Verifying tag: ${TAG}"
422495
423-
# Verify major tag
424-
echo "Checking major tag..."
425-
docker manifest inspect ${ALIYUN_IMAGE_NAME}:${MAJOR} || echo "Warning: Failed to verify major tag"
496+
# Extract architecture list from manifest using jq
497+
ARCHITECTURES=$(docker manifest inspect ${ALIYUN_IMAGE_NAME}:${TAG} 2>/dev/null | jq -r '.[].Platform[].architecture' 2>/dev/null || echo "")
426498
427-
# Verify latest tag (only for stable releases)
428-
if [ "$IS_STABLE" = "true" ]; then
429-
echo "Checking latest tag..."
430-
docker manifest inspect ${ALIYUN_IMAGE_NAME}:latest || echo "Warning: Failed to verify latest tag"
431-
fi
499+
if [ -z "${ARCHITECTURES}" ]; then
500+
echo "::error::Failed to inspect manifest for tag ${TAG} in Aliyun ACR"
501+
exit 1
502+
fi
503+
504+
echo "Found architectures for tag ${TAG}: ${ARCHITECTURES}"
505+
506+
# Check if amd64 is present (required for AMD64/x86_64 platforms)
507+
if ! echo "${ARCHITECTURES}" | grep -q "amd64"; then
508+
echo "::error::Tag ${TAG} in Aliyun ACR does not contain amd64 architecture"
509+
exit 1
510+
fi
511+
512+
# Check if arm64 is present (required for ARM64/AArch64 platforms)
513+
if ! echo "${ARCHITECTURES}" | grep -q "arm64"; then
514+
echo "::error::Tag ${TAG} in Aliyun ACR does not contain arm64 architecture"
515+
exit 1
516+
fi
517+
518+
echo "✓ Tag ${TAG} contains both amd64 and arm64 architectures"
519+
done
432520
433521
echo "Aliyun ACR image verification completed!"
434522

AGENTS.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,49 @@ AI agents are available in all Docker images pushed to registries, enabling:
111111
- Spec-driven workflows for changes
112112
- Multi-architecture support for AI tools
113113

114+
## Version Format Requirements
115+
116+
**Version Format Validation**: This repository enforces strict version number formatting to ensure consistency across the release pipeline.
117+
118+
### Required Version Format
119+
120+
All version numbers must:
121+
- **Start with a digit** (no "v" prefix allowed)
122+
- **Contain only** alphanumeric characters, dots (.), hyphens (-), or underscores (_)
123+
- **Follow semantic versioning** (semver) convention (e.g., `1.2.3`)
124+
125+
### Valid Examples
126+
- `1.2.3` - Standard semver
127+
- `0.1.0` - Leading zero allowed
128+
- `1.2.3-beta.1` - Pre-release identifier
129+
- `1.2.3-rc.1` - Release candidate
130+
- `1.0.0-alpha` - Alpha release
131+
132+
### Invalid Examples (will be rejected)
133+
- `v1.2.3` - v prefix is NOT allowed
134+
- `1.2.3 beta` - Contains space
135+
- `1.2.3@feature` - Contains special character @
136+
- `1/2/3` - Contains slash
137+
- `` - Empty string
138+
139+
### Validation Points
140+
141+
1. **Version Monitor** (`Build.Targets.VersionMonitor.cs`)
142+
- Validates versions from Azure Blob Storage
143+
- Skips invalid versions with warning logs
144+
145+
2. **Docker Build Workflow** (`.github/workflows/docker-build.yml`)
146+
- Validates version format in "Determine Version and Platform" step
147+
- Fails workflow with clear error message if format is invalid
148+
149+
### Error Message Example
150+
151+
```
152+
Error: Invalid version format 'v1.2.3'
153+
Version must start with a digit and only contain letters, numbers, dots, hyphens, or underscores.
154+
Example: 1.2.3, 1.2.3-beta.1
155+
```
156+
114157
## Troubleshooting
115158

116159
### Claude Code Not Working

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The HagiCode release repository manages:
2222
- Azure Blob Storage access (SAS URL)
2323
- GitHub personal access token (PAT)
2424
- Edge ACR credentials (username, password, registry)
25+
- jq (for JSON parsing in multi-arch verification)
2526

2627
### Building Locally
2728

@@ -30,7 +31,7 @@ The HagiCode release repository manages:
3031
git clone https://github.com/your-org/hagicode-release.git
3132
cd hagicode-release
3233

33-
# Run Nuke build
34+
# Run Nuke build (use version without v prefix)
3435
./build.sh DockerRelease \
3536
--ReleaseVersion "1.2.3" \
3637
--AzureBlobSasUrl "https://..." \
@@ -40,6 +41,17 @@ cd hagicode-release
4041
--DockerPlatform "all"
4142
```
4243

44+
### Version Format Requirements
45+
46+
**Important**: Version numbers must NOT include a "v" prefix.
47+
48+
- **Correct**: `1.2.3`, `1.2.3-beta.1`
49+
- **Incorrect**: `v1.2.3`, `1.2.3 beta`
50+
51+
Version format is validated automatically in:
52+
1. Version Monitor (skips invalid versions with warning)
53+
2. Docker Build workflow (fails with error message)
54+
4355
## Build Targets
4456

4557
### Available Targets
@@ -366,7 +378,22 @@ docker run -v ~/claude-config:/claude-mount hagicode.azurecr.io/hagicode:1.2.3
366378
4. **App Dockerfile Generation**: Generate Dockerfile from template with version injection
367379
5. **Application Image Build**: Build application image with base image
368380
6. **Edge ACR Push**: Push images to registry with version tags
369-
7. **Verification**: Verify images are available in registry
381+
7. **Multi-Architecture Verification**: Verify images contain both amd64 and arm64 architectures
382+
8. **Aliyun ACR Sync**: Replicate images to Aliyun Container Registry with verification
383+
384+
### Multi-Architecture Verification
385+
386+
All published Docker images are verified to contain both linux/amd64 and linux/arm64 architectures:
387+
388+
- **Azure ACR**: After push, `Verify Images in Registry` step checks manifest for both architectures
389+
- **Aliyun ACR**: After sync, `Verify Images in Aliyun ACR` step verifies all pushed tags
390+
391+
If either architecture is missing, the workflow fails with a clear error message:
392+
```
393+
Error: Tag 1.2.3 does not contain amd64 architecture
394+
```
395+
396+
This ensures users on both AMD64 and ARM64 platforms can pull compatible images.
370397

371398
### Version Tagging Strategy
372399

nukeBuild/Build.Targets.VersionMonitor.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.Linq;
77
using System.Text.Json;
8+
using System.Text.RegularExpressions;
89

910
/// <summary>
1011
/// Version Monitor target - monitors Azure Blob Storage for new versions and triggers GitHub releases
@@ -153,13 +154,47 @@ List<string> GetGitHubReleases(string token, string repository)
153154
}
154155
}
155156

157+
/// <summary>
158+
/// Validates version format to ensure it conforms to Docker tag naming conventions.
159+
/// Valid versions must start with a digit and contain only alphanumeric characters, dots, hyphens, or underscores.
160+
/// This ensures version numbers do not have a "v" prefix which would cause inconsistencies in the build workflow.
161+
/// </summary>
162+
/// <param name="version">The version string to validate</param>
163+
/// <returns>True if the version format is valid, false otherwise</returns>
164+
bool IsValidVersionFormat(string version)
165+
{
166+
// Docker tag specification: only allows letters, numbers, dots, hyphens, underscores
167+
// Must start with a digit (to ensure no "v" prefix)
168+
if (string.IsNullOrWhiteSpace(version))
169+
return false;
170+
171+
// Trim whitespace
172+
var trimmedVersion = version.Trim();
173+
174+
// Must start with a digit (ensures no "v" prefix)
175+
if (!char.IsDigit(trimmedVersion[0]))
176+
return false;
177+
178+
// Allowed characters: numbers, letters, dots, hyphens, underscores
179+
var allowedPattern = @"^[0-9A-Za-z._-]+$";
180+
return Regex.IsMatch(trimmedVersion, allowedPattern);
181+
}
182+
156183
List<string> FindNewVersions(List<string> azureVersions, List<string> githubReleases)
157184
{
158185
var newVersions = new List<string>();
159186

160187
foreach (var version in azureVersions)
161188
{
189+
// Validate version format before processing
190+
if (!IsValidVersionFormat(version))
191+
{
192+
Log.Warning("Skipping invalid version format: {Version} (must start with digit and contain only letters, numbers, dots, hyphens, or underscores)", version);
193+
continue;
194+
}
195+
162196
// Check if this version (with or without 'v' prefix) exists in GitHub releases
197+
// This is for backward compatibility with existing releases that may have v-prefixed tags
163198
var versionWithV = $"v{version}";
164199
var hasVersion = githubReleases.Contains(version, StringComparer.OrdinalIgnoreCase) ||
165200
githubReleases.Contains(versionWithV, StringComparer.OrdinalIgnoreCase);

0 commit comments

Comments
 (0)