save progress
This commit is contained in:
490
.gitea/workflows/service-release.yml
Normal file
490
.gitea/workflows/service-release.yml
Normal file
@@ -0,0 +1,490 @@
|
||||
# Service Release Pipeline
|
||||
# Sprint: CI/CD Enhancement - Per-Service Auto-Versioning
|
||||
#
|
||||
# Purpose: Automated per-service release pipeline with semantic versioning
|
||||
# and Docker tag format: {semver}+{YYYYMMDDHHmmss}
|
||||
#
|
||||
# Triggers:
|
||||
# - Tag: service-{name}-v{semver} (e.g., service-scanner-v1.2.3)
|
||||
# - Manual dispatch with service selection and bump type
|
||||
|
||||
name: Service Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'service-*-v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
service:
|
||||
description: 'Service to release'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- authority
|
||||
- attestor
|
||||
- concelier
|
||||
- scanner
|
||||
- policy
|
||||
- signer
|
||||
- excititor
|
||||
- gateway
|
||||
- scheduler
|
||||
- cli
|
||||
- orchestrator
|
||||
- notify
|
||||
- sbomservice
|
||||
- vexhub
|
||||
- evidencelocker
|
||||
bump_type:
|
||||
description: 'Version bump type'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
default: 'patch'
|
||||
dry_run:
|
||||
description: 'Dry run (no actual release)'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
skip_tests:
|
||||
description: 'Skip tests (use with caution)'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
DOTNET_VERSION: '10.0.100'
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
REGISTRY: git.stella-ops.org/stella-ops.org
|
||||
SYFT_VERSION: '1.21.0'
|
||||
|
||||
jobs:
|
||||
# ===========================================================================
|
||||
# Parse tag or manual inputs to determine service and version
|
||||
# ===========================================================================
|
||||
resolve:
|
||||
name: Resolve Release Parameters
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
service: ${{ steps.resolve.outputs.service }}
|
||||
bump_type: ${{ steps.resolve.outputs.bump_type }}
|
||||
current_version: ${{ steps.resolve.outputs.current_version }}
|
||||
new_version: ${{ steps.resolve.outputs.new_version }}
|
||||
docker_tag: ${{ steps.resolve.outputs.docker_tag }}
|
||||
is_dry_run: ${{ steps.resolve.outputs.is_dry_run }}
|
||||
skip_tests: ${{ steps.resolve.outputs.skip_tests }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve parameters
|
||||
id: resolve
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" ]]; then
|
||||
# Parse tag: service-{name}-v{version}
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
echo "Processing tag: $TAG"
|
||||
|
||||
if [[ "$TAG" =~ ^service-([a-z]+)-v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||
SERVICE="${BASH_REMATCH[1]}"
|
||||
VERSION="${BASH_REMATCH[2]}"
|
||||
BUMP_TYPE="explicit"
|
||||
else
|
||||
echo "::error::Invalid tag format: $TAG (expected: service-{name}-v{semver})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IS_DRY_RUN="false"
|
||||
SKIP_TESTS="false"
|
||||
else
|
||||
# Manual dispatch
|
||||
SERVICE="${{ github.event.inputs.service }}"
|
||||
BUMP_TYPE="${{ github.event.inputs.bump_type }}"
|
||||
VERSION="" # Will be calculated
|
||||
IS_DRY_RUN="${{ github.event.inputs.dry_run }}"
|
||||
SKIP_TESTS="${{ github.event.inputs.skip_tests }}"
|
||||
fi
|
||||
|
||||
# Read current version
|
||||
CURRENT_VERSION=$(.gitea/scripts/release/read-service-version.sh "$SERVICE")
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
# Calculate new version
|
||||
if [[ -n "$VERSION" ]]; then
|
||||
NEW_VERSION="$VERSION"
|
||||
else
|
||||
NEW_VERSION=$(python3 .gitea/scripts/release/bump-service-version.py "$SERVICE" "$BUMP_TYPE" --output-version)
|
||||
fi
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# Generate Docker tag
|
||||
DOCKER_TAG=$(.gitea/scripts/release/generate-docker-tag.sh --version "$NEW_VERSION")
|
||||
echo "Docker tag: $DOCKER_TAG"
|
||||
|
||||
# Set outputs
|
||||
echo "service=$SERVICE" >> $GITHUB_OUTPUT
|
||||
echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT
|
||||
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "docker_tag=$DOCKER_TAG" >> $GITHUB_OUTPUT
|
||||
echo "is_dry_run=$IS_DRY_RUN" >> $GITHUB_OUTPUT
|
||||
echo "skip_tests=$SKIP_TESTS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## Release Parameters" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Parameter | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-----------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Service | ${{ steps.resolve.outputs.service }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Current Version | ${{ steps.resolve.outputs.current_version }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| New Version | ${{ steps.resolve.outputs.new_version }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Docker Tag | ${{ steps.resolve.outputs.docker_tag }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Dry Run | ${{ steps.resolve.outputs.is_dry_run }} |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ===========================================================================
|
||||
# Update version in source files
|
||||
# ===========================================================================
|
||||
update-version:
|
||||
name: Update Version
|
||||
runs-on: ubuntu-latest
|
||||
needs: [resolve]
|
||||
if: needs.resolve.outputs.is_dry_run != 'true'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Update version
|
||||
run: |
|
||||
python3 .gitea/scripts/release/bump-service-version.py \
|
||||
"${{ needs.resolve.outputs.service }}" \
|
||||
"${{ needs.resolve.outputs.new_version }}" \
|
||||
--docker-tag "${{ needs.resolve.outputs.docker_tag }}" \
|
||||
--git-sha "${{ github.sha }}"
|
||||
|
||||
- name: Commit version update
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add src/Directory.Versions.props devops/releases/service-versions.json
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "No version changes to commit"
|
||||
else
|
||||
git commit -m "chore(${{ needs.resolve.outputs.service }}): release v${{ needs.resolve.outputs.new_version }}
|
||||
|
||||
Docker tag: ${{ needs.resolve.outputs.docker_tag }}
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
|
||||
Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
||||
|
||||
git push
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
# Build and test the service
|
||||
# ===========================================================================
|
||||
build-test:
|
||||
name: Build and Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: [resolve, update-version]
|
||||
if: always() && (needs.update-version.result == 'success' || needs.update-version.result == 'skipped')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
include-prerelease: true
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore src/StellaOps.sln
|
||||
|
||||
- name: Build solution
|
||||
run: |
|
||||
dotnet build src/StellaOps.sln \
|
||||
--configuration Release \
|
||||
--no-restore \
|
||||
-p:StellaOpsServiceVersion=${{ needs.resolve.outputs.new_version }}
|
||||
|
||||
- name: Run tests
|
||||
if: needs.resolve.outputs.skip_tests != 'true'
|
||||
run: |
|
||||
SERVICE="${{ needs.resolve.outputs.service }}"
|
||||
SERVICE_PASCAL=$(echo "$SERVICE" | sed -r 's/(^|-)(\w)/\U\2/g')
|
||||
|
||||
# Find and run tests for this service
|
||||
TEST_PROJECTS=$(find src -path "*/${SERVICE_PASCAL}/*" -name "*.Tests.csproj" -o -path "*/${SERVICE_PASCAL}*Tests*" -name "*.csproj" | head -20)
|
||||
|
||||
if [[ -n "$TEST_PROJECTS" ]]; then
|
||||
echo "Running tests for: $TEST_PROJECTS"
|
||||
echo "$TEST_PROJECTS" | xargs -I{} dotnet test {} --configuration Release --no-build --verbosity normal
|
||||
else
|
||||
echo "::warning::No test projects found for service: $SERVICE"
|
||||
fi
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-${{ needs.resolve.outputs.service }}
|
||||
path: |
|
||||
src/**/bin/Release/**/*.dll
|
||||
src/**/bin/Release/**/*.exe
|
||||
src/**/bin/Release/**/*.pdb
|
||||
retention-days: 7
|
||||
|
||||
# ===========================================================================
|
||||
# Build and publish Docker image
|
||||
# ===========================================================================
|
||||
publish-container:
|
||||
name: Publish Container
|
||||
runs-on: ubuntu-latest
|
||||
needs: [resolve, build-test]
|
||||
if: needs.resolve.outputs.is_dry_run != 'true'
|
||||
outputs:
|
||||
image_digest: ${{ steps.push.outputs.digest }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Determine Dockerfile path
|
||||
id: dockerfile
|
||||
run: |
|
||||
SERVICE="${{ needs.resolve.outputs.service }}"
|
||||
SERVICE_PASCAL=$(echo "$SERVICE" | sed -r 's/(^|-)(\w)/\U\2/g')
|
||||
|
||||
# Look for service-specific Dockerfile
|
||||
DOCKERFILE_PATHS=(
|
||||
"devops/docker/${SERVICE}/Dockerfile"
|
||||
"devops/docker/${SERVICE_PASCAL}/Dockerfile"
|
||||
"src/${SERVICE_PASCAL}/Dockerfile"
|
||||
"src/${SERVICE_PASCAL}/StellaOps.${SERVICE_PASCAL}.WebService/Dockerfile"
|
||||
"devops/docker/platform/Dockerfile"
|
||||
)
|
||||
|
||||
for path in "${DOCKERFILE_PATHS[@]}"; do
|
||||
if [[ -f "$path" ]]; then
|
||||
echo "dockerfile=$path" >> $GITHUB_OUTPUT
|
||||
echo "Found Dockerfile: $path"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "::error::No Dockerfile found for service: $SERVICE"
|
||||
exit 1
|
||||
|
||||
- name: Build and push image
|
||||
id: push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ steps.dockerfile.outputs.dockerfile }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}:${{ needs.resolve.outputs.docker_tag }}
|
||||
${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}:${{ needs.resolve.outputs.new_version }}
|
||||
${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}:latest
|
||||
labels: |
|
||||
org.opencontainers.image.title=${{ needs.resolve.outputs.service }}
|
||||
org.opencontainers.image.version=${{ needs.resolve.outputs.new_version }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||
com.stellaops.service.name=${{ needs.resolve.outputs.service }}
|
||||
com.stellaops.service.version=${{ needs.resolve.outputs.new_version }}
|
||||
com.stellaops.docker.tag=${{ needs.resolve.outputs.docker_tag }}
|
||||
build-args: |
|
||||
VERSION=${{ needs.resolve.outputs.new_version }}
|
||||
GIT_SHA=${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Image summary
|
||||
run: |
|
||||
echo "## Container Image" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Image | \`${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Tag | \`${{ needs.resolve.outputs.docker_tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Digest | \`${{ steps.push.outputs.digest }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ===========================================================================
|
||||
# Generate SBOM
|
||||
# ===========================================================================
|
||||
generate-sbom:
|
||||
name: Generate SBOM
|
||||
runs-on: ubuntu-latest
|
||||
needs: [resolve, publish-container]
|
||||
if: needs.resolve.outputs.is_dry_run != 'true'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Syft
|
||||
run: |
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | \
|
||||
sh -s -- -b /usr/local/bin v${{ env.SYFT_VERSION }}
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Generate SBOM
|
||||
run: |
|
||||
IMAGE="${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}:${{ needs.resolve.outputs.docker_tag }}"
|
||||
|
||||
syft "$IMAGE" \
|
||||
--output cyclonedx-json=sbom.cyclonedx.json \
|
||||
--output spdx-json=sbom.spdx.json
|
||||
|
||||
echo "Generated SBOMs for: $IMAGE"
|
||||
|
||||
- name: Upload SBOM artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sbom-${{ needs.resolve.outputs.service }}-${{ needs.resolve.outputs.new_version }}
|
||||
path: |
|
||||
sbom.cyclonedx.json
|
||||
sbom.spdx.json
|
||||
retention-days: 90
|
||||
|
||||
# ===========================================================================
|
||||
# Sign artifacts with Cosign
|
||||
# ===========================================================================
|
||||
sign-artifacts:
|
||||
name: Sign Artifacts
|
||||
runs-on: ubuntu-latest
|
||||
needs: [resolve, publish-container, generate-sbom]
|
||||
if: needs.resolve.outputs.is_dry_run != 'true'
|
||||
|
||||
steps:
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Sign container image
|
||||
if: env.COSIGN_PRIVATE_KEY_B64 != ''
|
||||
env:
|
||||
COSIGN_PRIVATE_KEY_B64: ${{ secrets.COSIGN_PRIVATE_KEY_B64 }}
|
||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||
run: |
|
||||
echo "$COSIGN_PRIVATE_KEY_B64" | base64 -d > cosign.key
|
||||
|
||||
IMAGE="${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}@${{ needs.publish-container.outputs.image_digest }}"
|
||||
|
||||
cosign sign --key cosign.key \
|
||||
-a "service=${{ needs.resolve.outputs.service }}" \
|
||||
-a "version=${{ needs.resolve.outputs.new_version }}" \
|
||||
-a "docker-tag=${{ needs.resolve.outputs.docker_tag }}" \
|
||||
"$IMAGE"
|
||||
|
||||
rm -f cosign.key
|
||||
echo "Signed: $IMAGE"
|
||||
|
||||
- name: Download SBOM
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sbom-${{ needs.resolve.outputs.service }}-${{ needs.resolve.outputs.new_version }}
|
||||
path: sbom/
|
||||
|
||||
- name: Attach SBOM to image
|
||||
if: env.COSIGN_PRIVATE_KEY_B64 != ''
|
||||
env:
|
||||
COSIGN_PRIVATE_KEY_B64: ${{ secrets.COSIGN_PRIVATE_KEY_B64 }}
|
||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||
run: |
|
||||
echo "$COSIGN_PRIVATE_KEY_B64" | base64 -d > cosign.key
|
||||
|
||||
IMAGE="${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}@${{ needs.publish-container.outputs.image_digest }}"
|
||||
|
||||
cosign attach sbom --sbom sbom/sbom.cyclonedx.json "$IMAGE"
|
||||
cosign sign --key cosign.key --attachment sbom "$IMAGE"
|
||||
|
||||
rm -f cosign.key
|
||||
|
||||
# ===========================================================================
|
||||
# Release summary
|
||||
# ===========================================================================
|
||||
summary:
|
||||
name: Release Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [resolve, build-test, publish-container, generate-sbom, sign-artifacts]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Generate summary
|
||||
run: |
|
||||
echo "# Service Release: ${{ needs.resolve.outputs.service }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Release Details" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Service | ${{ needs.resolve.outputs.service }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | ${{ needs.resolve.outputs.new_version }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Previous | ${{ needs.resolve.outputs.current_version }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Docker Tag | \`${{ needs.resolve.outputs.docker_tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Git SHA | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Job Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Build & Test | ${{ needs.build-test.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Publish Container | ${{ needs.publish-container.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Generate SBOM | ${{ needs.generate-sbom.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Sign Artifacts | ${{ needs.sign-artifacts.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ "${{ needs.resolve.outputs.is_dry_run }}" == "true" ]]; then
|
||||
echo "⚠️ **This was a dry run. No artifacts were published.**" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "## Pull Image" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
|
||||
echo "docker pull ${{ env.REGISTRY }}/${{ needs.resolve.outputs.service }}:${{ needs.resolve.outputs.docker_tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
Reference in New Issue
Block a user