From 33f1978bd0b0bfbd4ee03d6c7fc314db1f25987c Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Thu, 2 Apr 2026 23:54:52 +0000 Subject: [PATCH] fix(ci): resolve repository push failure in CI pipeline 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 --- .forgejo/workflows/ci.yml | 85 +++++++++++++++++++++++- .forgejo/workflows/release.yml | 60 ++++++++++++++++- docs/development/ci-cd.md | 115 +++++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+), 3 deletions(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 712a8231f..e918069ff 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -553,9 +553,88 @@ jobs: -summary \ /tmp/rendered.yaml + + push-validation: + # Validates that the CI runner can authenticate and push to the repository. + # Root cause of the push failure: actions/checkout@v4 was not configured with + # explicit push credentials (token + persist-credentials), and no git user + # config (name/email) was set — both are required for any push operation. + runs-on: docker + container: + image: python:3.13-slim + steps: + - name: Install system dependencies (nodejs for checkout, git for push validation) + run: | + apt-get update && apt-get install -y -qq nodejs git curl && rm -rf /var/lib/apt/lists/* + + - name: Checkout with explicit write credentials + uses: actions/checkout@v4 + with: + # Pass the Forgejo token explicitly so the credential helper is + # configured for HTTPS push operations. Without this, the default + # checkout may only have read access and push will fail with a + # 403 or authentication error. + token: ${{ secrets.FORGEJO_TOKEN }} + persist-credentials: true + + - name: Configure git user for CI operations + run: | + # Required for any git commit or push operation in CI. + # Uses a bot identity to distinguish CI-generated commits from + # human commits. Without this, git push fails with: + # "Author identity unknown — please tell me who you are." + git config user.name "CleverAgents CI" + git config user.email "ci-bot@cleverthis.com" + + - name: Verify HTTPS credential helper is configured + run: | + # Confirm that the credential helper set up by actions/checkout + # is active. This ensures HTTPS push operations will authenticate + # correctly without prompting for a password. + echo "=== Git credential configuration ===" + git config --list | grep -E "credential|url" || echo "WARNING: No credential helper found" + echo "=== Remote URL ===" + git remote get-url origin + echo "=== Credential helper check ===" + if git config credential.helper > /dev/null 2>&1; then + echo "OK: Credential helper is configured: $(git config credential.helper)" + else + echo "WARNING: No credential helper configured — push may fail" + fi + + - name: Smoke-test push access via API + # Validates write permission using the Forgejo API before attempting + # any real push. This catches credential issues early with a clear + # error message rather than a cryptic git error. + env: + FORGEJO_URL: ${{ secrets.FORGEJO_URL }} + FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} + run: | + REPO="${{ forgejo.repository }}" + API_URL="${FORGEJO_URL}/api/v1/repos/${REPO}" + echo "=== Testing repository API access ===" + 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 FORGEJO_TOKEN is set in Repository Settings > Actions > Secrets" + echo "and that the token has repository (write) scope." + exit 1 + fi + 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." + echo "Grant the token Contents: Write permission or use a token with full repository scope." + exit 1 + fi + echo "OK: Push access verified -- FORGEJO_TOKEN has write permission on ${REPO}" + echo "=== Push access smoke-test passed ===" status-check: if: always() - needs: [lint, typecheck, security, quality, unit_tests, integration_tests, e2e_tests, coverage, build, docker, helm] + needs: [lint, typecheck, security, quality, unit_tests, integration_tests, e2e_tests, coverage, build, docker, helm, push-validation] runs-on: docker container: image: python:3.13-slim @@ -573,6 +652,7 @@ jobs: echo "build: ${{ needs.build.result }}" echo "docker: ${{ needs.docker.result }}" echo "helm: ${{ needs.helm.result }}" + echo "push-validation: ${{ needs.push-validation.result }}" if [ "${{ needs.lint.result }}" != "success" ] || \ [ "${{ needs.typecheck.result }}" != "success" ] || \ @@ -584,7 +664,8 @@ jobs: [ "${{ needs.coverage.result }}" != "success" ] || \ [ "${{ needs.build.result }}" != "success" ] || \ [ "${{ needs.docker.result }}" != "success" ] || \ - [ "${{ needs.helm.result }}" != "success" ]; then + [ "${{ needs.helm.result }}" != "success" ] || \ + [ "${{ needs.push-validation.result }}" != "success" ]; then echo "FAILED: One or more required jobs did not succeed" exit 1 fi diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 60f5dcffe..ad817e49e 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -90,9 +90,67 @@ jobs: steps: - name: Install system dependencies run: | - apt-get update && apt-get install -y -qq nodejs curl jq && rm -rf /var/lib/apt/lists/* + 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 diff --git a/docs/development/ci-cd.md b/docs/development/ci-cd.md index 6f6bf7486..b75667bb2 100644 --- a/docs/development/ci-cd.md +++ b/docs/development/ci-cd.md @@ -162,6 +162,7 @@ unit_tests ────────────┘ integration_tests ──────── (independent) quality ────────────────── (independent) build ──────────────────── (independent) +push-validation ────────── (independent — validates CI push credentials) ``` ### Nox-Based CI @@ -186,6 +187,7 @@ All gates must pass for a PR to be mergeable: | Unit Tests | All pass (3.11-3.13) | `behave` job | | Coverage | >= 97% | `coverage` job (nox) | | Build | Wheel builds | `build` job | +| Push Access | Credentials valid | `push-validation` job | ### Nightly Quality Monitoring @@ -258,6 +260,11 @@ in job logs. | `AWS_SECRET_ACCESS_KEY` | `benchmark-regression`, `benchmark-publish` | AWS credentials for ASV benchmark S3 storage | | `AWS_DEFAULT_REGION` | `benchmark-regression`, `benchmark-publish` | AWS region for ASV benchmark S3 storage | | `ASV_S3_BUCKET` | `benchmark-regression`, `benchmark-publish` | S3 bucket name for ASV benchmark storage | +| `FORGEJO_TOKEN` | `release` (`create-release` job) | Forgejo API token with **repository write** scope — used to authenticate `git push` and create releases | +| `FORGEJO_URL` | `release` (`create-release` job) | Base URL of the Forgejo instance (e.g., `https://git.cleverthis.com`) | +| `CONTAINER_REGISTRY` | `release` (`build-docker` job) | Container registry URL for Docker image pushes | +| `CONTAINER_REGISTRY_USER` | `release` (`build-docker` job) | Username for container registry authentication | +| `CONTAINER_REGISTRY_PASSWORD` | `release` (`build-docker` job) | Password/token for container registry authentication | **Unit tests vs. integration tests:** @@ -270,6 +277,114 @@ in job logs. If the LLM secrets are not configured, integration tests will fail with authentication errors at runtime. +### Repository Push Authentication + +Any CI workflow step that writes back to the repository (e.g., pushing tags, +committing auto-generated files, or creating changelog commits) **must** use +explicit token authentication. The `push-validation` job in `ci.yml` validates +that push credentials are correctly configured on every CI run. + +**Fix applied (issue #1541):** The `actions/checkout@v4` action was not +configured with `token: ${{ forgejo.token }}` and `persist-credentials: true`, +and no git user config (`user.name` / `user.email`) was set. Both are required +for push operations. The `push-validation` job now validates these on every run. + +#### Root Cause of "Unable to Push" Failures + +The most common cause of CI push failures is missing or misconfigured +credentials. Symptoms include: + +``` +remote: Permission denied +fatal: unable to access 'https://...': The requested URL returned error: 403 +``` + +or: + +``` +ERROR: Repository not found. +fatal: Could not read from remote repository. +``` + +#### Fix: HTTPS Token Authentication + +The canonical fix is to configure git to use the `FORGEJO_TOKEN` secret via +HTTPS credential store. This is done in the `create-release` job of +`release.yml`: + +```yaml +- uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.FORGEJO_TOKEN }} # Use write-scoped token + +- name: Configure git identity for push operations + 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" + 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 +``` + +#### Smoke-Test Step + +The `create-release` job includes a smoke-test step that validates push access +**before** attempting the real push. This catches credential issues early with +a clear error message: + +```yaml +- name: Smoke-test push access + 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})." + exit 1 + fi + 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." + exit 1 + fi + echo "Push access verified." +``` + +#### Setting Up the FORGEJO_TOKEN Secret + +1. Create a Forgejo personal access token (PAT) with **repository write** scope: + - Navigate to **User Settings** > **Applications** > **Access Tokens** + - Create a token with `repository` scope (read + write) + - Copy the token value (shown only once) + +2. Add the token as a repository secret: + - Navigate to **Repository Settings** > **Actions** > **Secrets** + - Add secret `FORGEJO_TOKEN` with the PAT value + - Add secret `FORGEJO_URL` with the Forgejo base URL (e.g., `https://git.cleverthis.com`) + +3. **Never hardcode tokens** in workflow files. Always use `${{ secrets.SECRET_NAME }}`. + +#### Security Notes + +- The `FORGEJO_TOKEN` secret is automatically masked in job logs by Forgejo Actions. +- The `~/.git-credentials` file is created with `chmod 600` (owner-read-only). +- The credential file is ephemeral — it exists only for the duration of the job + in the container's filesystem and is destroyed when the container exits. +- No SSH deploy keys are required; HTTPS token authentication is sufficient and + simpler to manage. + ### Pre-commit Hooks Pre-commit hooks run automatically on `git commit` and catch most issues before -- 2.52.0