name: Build and Push on: push: branches: [main] # Don't rebuild on doc-only or CI-config-only changes paths-ignore: - 'README.md' - '.gitea/**' - 'deploy/**' workflow_dispatch: env: REGISTRY: registry.c5ai.ch IMAGE: pieced/pieced-portal jobs: build: # 'self-hosted' matches the label our act_runner registers with. # 'ubuntu-latest' would work too because we configure both labels in the # runner config, but self-hosted makes intent explicit. runs-on: ubuntu-latest env: DOCKER_HOST: tcp://172.17.0.1:2375 outputs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout uses: actions/checkout@v4 - name: Determine next patch version id: version # Reads tags from the registry's OCI Distribution v2 API, filters to # strict semver (skips 'latest', 'dev', '-dirty', etc.), picks the # highest with version-sort, and bumps the patch component. If nothing # numeric exists yet (fresh registry), starts at 0.1.0. env: REG_USER: ${{ secrets.REGISTRY_USERNAME }} REG_PASS: ${{ secrets.REGISTRY_PASSWORD }} run: | set -euo pipefail tags_json=$(curl -sf -u "$REG_USER:$REG_PASS" \ "https://${REGISTRY}/v2/${IMAGE}/tags/list") highest=$(echo "$tags_json" \ | jq -r '.tags // [] | .[]' \ | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ | sort -V \ | tail -n1 || true) if [ -z "$highest" ]; then next="0.1.0" echo "No semver tags found — starting at $next" else major=$(echo "$highest" | cut -d. -f1) minor=$(echo "$highest" | cut -d. -f2) patch=$(echo "$highest" | cut -d. -f3) next="${major}.${minor}.$((patch + 1))" echo "Highest existing: $highest → next: $next" fi echo "version=${next}" >> "$GITHUB_OUTPUT" - name: Diagnose push failure env: REG_USER: ${{ secrets.REGISTRY_USERNAME }} REG_PASS: ${{ secrets.REGISTRY_PASSWORD }} VERSION: ${{ steps.version.outputs.version }} run: | set +e echo "=== 1. Auth value lengths ===" echo "USER length: ${#REG_USER}" echo "PASS length: ${#REG_PASS}" echo echo "=== 2. Test creds with curl POST blobs/uploads ===" curl_resp=$(curl -s -o /dev/null -w 'http_code=%{http_code}' \ -u "$REG_USER:$REG_PASS" -X POST \ "https://${REGISTRY}/v2/${IMAGE}/blobs/uploads/") echo "$curl_resp" echo echo "=== 3. Docker login (verbose) ===" printf '%s' "$REG_PASS" | docker login "${REGISTRY}" -u "$REG_USER" --password-stdin echo "Exit code: $?" echo echo "=== 4. Decoded auth from docker config ===" if [ -f "$HOME/.docker/config.json" ]; then decoded=$(jq -r '.auths["registry.c5ai.ch"].auth // empty' "$HOME/.docker/config.json" | base64 -d 2>/dev/null) echo "Decoded length: ${#decoded}" # Verify it equals USER:PASS expected="${REG_USER}:${REG_PASS}" if [ "$decoded" = "$expected" ]; then echo "Stored auth matches expected USER:PASS" else echo "MISMATCH between stored auth and expected" echo "Expected length: ${#expected}, stored length: ${#decoded}" fi fi echo echo "=== 5. Pull tiny image (proves daemon connectivity) ===" docker pull alpine:3.20 2>&1 | tail -3 echo echo "=== 6. Push tiny image ===" docker tag alpine:3.20 "${REGISTRY}/${IMAGE}:debug-tiny" docker push "${REGISTRY}/${IMAGE}:debug-tiny" 2>&1 | tail -10 echo "Exit code: $?" echo echo "=== 7. Direct PUT manifest using curl (manifest endpoint) ===" # If a layer exists already in the registry, we can test manifest auth alone curl -s -o /dev/null -w 'http_code=%{http_code}\n' \ -u "$REG_USER:$REG_PASS" \ "https://${REGISTRY}/v2/${IMAGE}/manifests/0.1.4" echo echo "=== 8. PATCH endpoint test (the operation that fails during push) ===" # First initiate an upload to get a session URL loc=$(curl -s -i -u "$REG_USER:$REG_PASS" -X POST \ "https://${REGISTRY}/v2/${IMAGE}/blobs/uploads/" | grep -i '^location:' | tr -d '\r' | awk '{print $2}') echo "Upload location: $loc" - name: Build and push image # Combine login + build + push in a single run block. act_runner can # use ephemeral per-step containers in some configurations, in which # case `docker login` from one step doesn't leave its cached # ~/.docker/config.json visible to the next step. Doing everything # in one shell session sidesteps that entirely. env: REG_USER: ${{ secrets.REGISTRY_USERNAME }} REG_PASS: ${{ secrets.REGISTRY_PASSWORD }} VERSION: ${{ steps.version.outputs.version }} run: | set -euo pipefail printf '%s' "$REG_PASS" \ | docker login "${REGISTRY}" -u "$REG_USER" --password-stdin docker build \ --pull \ -t "${REGISTRY}/${IMAGE}:${VERSION}" \ -t "${REGISTRY}/${IMAGE}:latest" \ . docker push "${REGISTRY}/${IMAGE}:${VERSION}" docker push "${REGISTRY}/${IMAGE}:latest" - name: Tag git commit with version env: VERSION: ${{ steps.version.outputs.version }} run: | set -euo pipefail git config user.name "pieced-ci" git config user.email "ci@pieced.ch" git tag -a "v${VERSION}" -m "Release ${VERSION}" # Use CI_TOKEN explicitly so we can push a tag (the workflow's # default token may or may not have push scope depending on Gitea # actions config — explicit token avoids ambiguity). git push \ "https://oauth2:${{ secrets.CI_TOKEN }}@git.c5ai.ch/pieced/pieced-portal.git" \ "v${VERSION}" - name: Summary env: VERSION: ${{ steps.version.outputs.version }} run: | { echo "## Build complete: ${VERSION}" echo echo "**Image:** \`${REGISTRY}/${IMAGE}:${VERSION}\`" echo echo "Run the **Deploy to GitOps** workflow to roll this version out." } >> "$GITHUB_STEP_SUMMARY"