name: Release on: push: tags: - "v*" vars: docker_prefix: "http://harbor.cleverthis.com/docker/" env: UV_VERSION: "0.8.0" PYTHON_VERSION: "3.13" NOX_DEFAULT_VENV_BACKEND: "uv" jobs: build-wheel: runs-on: docker container: image: ${{vars.docker_prefix}}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: ${{vars.docker_prefix}}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