33f1978bd0
Root cause: actions/checkout@v4 was not configured with explicit write credentials (token + persist-credentials), and no git user identity (user.name/user.email) was set. Both are required for any git push operation in Forgejo Actions. Changes: - release.yml create-release job: add token: secrets.FORGEJO_TOKEN and fetch-depth: 0 to checkout; add 'Configure git identity for push operations' step using HTTPS credential store; add 'Smoke-test push access' step that validates write permission via Forgejo API before any push attempt - ci.yml: add push-validation job that validates push credentials on every CI run using FORGEJO_TOKEN, including credential helper verification and API-based write permission check; add push-validation to status-check needs and result reporting - docs/development/ci-cd.md: add FORGEJO_TOKEN, FORGEJO_URL, and CONTAINER_REGISTRY* secrets to the secrets table; add 'Repository Push Authentication' section documenting root cause, fix pattern, smoke-test step, setup instructions, and security notes; add push-validation to CI job dependency graph and quality gates table Design decisions: - HTTPS token authentication (not SSH deploy keys) -- simpler to manage - ~/.git-credentials with chmod 600 for ephemeral, secure storage - Smoke-test validates write permission via API before push attempts - push-validation job is independent (no needs) -- runs in parallel - No hardcoded credentials -- all secrets via Forgejo Secrets ISSUES CLOSED: #1541
200 lines
8.4 KiB
YAML
200 lines
8.4 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v*"
|
|
|
|
env:
|
|
UV_VERSION: "0.8.0"
|
|
PYTHON_VERSION: "3.13"
|
|
NOX_DEFAULT_VENV_BACKEND: "uv"
|
|
|
|
jobs:
|
|
build-wheel:
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
steps:
|
|
- name: Install Node.js (required by actions/checkout)
|
|
run: |
|
|
apt-get update && apt-get install -y -qq nodejs && rm -rf /var/lib/apt/lists/*
|
|
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install uv and nox
|
|
run: |
|
|
pip install -q uv==${{ env.UV_VERSION }} nox
|
|
|
|
- name: Build wheel via nox
|
|
run: |
|
|
nox -s build
|
|
env:
|
|
NOX_DEFAULT_VENV_BACKEND: uv
|
|
|
|
- name: Upload wheel artifact
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: wheel
|
|
path: dist/*.whl
|
|
retention-days: 30
|
|
|
|
build-docker:
|
|
runs-on: docker
|
|
container:
|
|
image: docker:dind
|
|
options: --privileged
|
|
needs: [build-wheel]
|
|
steps:
|
|
- name: Start Docker daemon and install dependencies
|
|
run: |
|
|
dockerd &
|
|
apk add --no-cache git nodejs
|
|
for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break || sleep 1; done
|
|
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Extract tag name
|
|
id: tag
|
|
run: |
|
|
TAG_NAME="${FORGEJO_REF_NAME:-${GITHUB_REF_NAME:-unknown}}"
|
|
echo "TAG_NAME=${TAG_NAME}" >> $FORGEJO_OUTPUT
|
|
|
|
- name: Build Docker image
|
|
run: |
|
|
TAG="${{ steps.tag.outputs.TAG_NAME }}"
|
|
docker build \
|
|
-t "${{ secrets.CONTAINER_REGISTRY }}:${TAG}" \
|
|
-t "${{ secrets.CONTAINER_REGISTRY }}:latest" \
|
|
.
|
|
|
|
- name: Push Docker image to registry
|
|
if: secrets.CONTAINER_REGISTRY != ''
|
|
run: |
|
|
TAG="${{ steps.tag.outputs.TAG_NAME }}"
|
|
if [ -n "${{ secrets.CONTAINER_REGISTRY_USER }}" ]; then
|
|
echo "${{ secrets.CONTAINER_REGISTRY_PASSWORD }}" | \
|
|
docker login \
|
|
-u "${{ secrets.CONTAINER_REGISTRY_USER }}" \
|
|
--password-stdin \
|
|
"${{ secrets.CONTAINER_REGISTRY }}"
|
|
fi
|
|
docker push "${{ secrets.CONTAINER_REGISTRY }}:${TAG}"
|
|
docker push "${{ secrets.CONTAINER_REGISTRY }}:latest"
|
|
|
|
create-release:
|
|
runs-on: docker
|
|
container:
|
|
image: python:3.13-slim
|
|
needs: [build-wheel, build-docker]
|
|
steps:
|
|
- name: Install system dependencies
|
|
run: |
|
|
apt-get update && apt-get install -y -qq nodejs curl jq git && rm -rf /var/lib/apt/lists/*
|
|
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
# Fetch full history so we can push tags and commits back
|
|
fetch-depth: 0
|
|
# Use FORGEJO_TOKEN so the checkout token has write scope.
|
|
# The default GITHUB_TOKEN / FORGEJO_TOKEN provided by the
|
|
# runner is read-only for push operations in many Forgejo
|
|
# configurations; supplying an explicit token with write
|
|
# permissions resolves the "unable to push" failure.
|
|
token: ${{ secrets.FORGEJO_TOKEN }}
|
|
|
|
- name: Configure git identity for push operations
|
|
# Required so that any git commit or tag created by this job
|
|
# has a valid author. Without this, `git push` may succeed but
|
|
# `git commit` (e.g., for changelog auto-commits) will fail.
|
|
env:
|
|
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
|
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
|
run: |
|
|
git config user.name "CleverAgents CI"
|
|
git config user.email "ci@cleverthis.com"
|
|
# Configure HTTPS credential helper so that git push uses
|
|
# the FORGEJO_TOKEN secret rather than the default (often
|
|
# read-only) runner token. This is the canonical fix for
|
|
# "unable to push to repository" in Forgejo Actions.
|
|
FORGEJO_HOST=$(echo "${FORGEJO_URL}" | sed 's|https\?://||' | cut -d/ -f1)
|
|
git config credential.helper store
|
|
echo "https://ci:${FORGEJO_TOKEN}@${FORGEJO_HOST}" > ~/.git-credentials
|
|
chmod 600 ~/.git-credentials
|
|
|
|
- name: Smoke-test push access
|
|
# Validates that the CI runner can push to the repository before
|
|
# attempting the real push steps. A failure here means the
|
|
# FORGEJO_TOKEN secret is missing, expired, or lacks write scope.
|
|
env:
|
|
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
|
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
|
run: |
|
|
REPO="${{ forgejo.repository }}"
|
|
API_URL="${FORGEJO_URL}/api/v1/repos/${REPO}"
|
|
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
"${API_URL}")
|
|
if [ "${HTTP_STATUS}" != "200" ]; then
|
|
echo "ERROR: FORGEJO_TOKEN cannot access repository API (HTTP ${HTTP_STATUS})."
|
|
echo "Ensure the FORGEJO_TOKEN secret is set in Repository Settings > Actions > Secrets"
|
|
echo "and that the token has 'repository' (write) scope."
|
|
exit 1
|
|
fi
|
|
# Verify push permission by checking the token's permissions
|
|
PUSH_ALLOWED=$(curl -s \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
"${API_URL}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(str(d.get('permissions',{}).get('push',False)).lower())")
|
|
if [ "${PUSH_ALLOWED}" != "true" ]; then
|
|
echo "ERROR: FORGEJO_TOKEN does not have push (write) permission on this repository."
|
|
echo "Grant the token 'Contents: Write' permission or use a token with full repository scope."
|
|
exit 1
|
|
fi
|
|
echo "Push access verified: FORGEJO_TOKEN has write permission on ${REPO}"
|
|
|
|
- name: Download wheel artifact
|
|
uses: actions/download-artifact@v3
|
|
with:
|
|
name: wheel
|
|
path: dist/
|
|
|
|
- name: Extract tag name
|
|
id: tag
|
|
run: |
|
|
TAG_NAME="${FORGEJO_REF_NAME:-${GITHUB_REF_NAME:-unknown}}"
|
|
echo "TAG_NAME=${TAG_NAME}" >> $FORGEJO_OUTPUT
|
|
|
|
- name: Create Forgejo release with wheel
|
|
env:
|
|
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
|
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
|
run: |
|
|
TAG="${{ steps.tag.outputs.TAG_NAME }}"
|
|
REPO="${{ forgejo.repository }}"
|
|
API_URL="${FORGEJO_URL}/api/v1/repos/${REPO}/releases"
|
|
|
|
# Create the release
|
|
RELEASE_ID=$(curl -s -X POST "${API_URL}" \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"tag_name\": \"${TAG}\",
|
|
\"name\": \"${TAG}\",
|
|
\"body\": \"Release ${TAG}\",
|
|
\"draft\": false,
|
|
\"prerelease\": false
|
|
}" | jq -r '.id')
|
|
|
|
echo "Created release ID: ${RELEASE_ID}"
|
|
|
|
# Attach wheel artifacts
|
|
for whl in dist/*.whl; do
|
|
FILENAME=$(basename "${whl}")
|
|
curl -s -X POST \
|
|
"${API_URL}/${RELEASE_ID}/assets?name=${FILENAME}" \
|
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
|
-H "Content-Type: application/octet-stream" \
|
|
--data-binary "@${whl}"
|
|
echo "Attached ${FILENAME} to release"
|
|
done
|