fix(ci): resolve repository push failure in CI pipeline #1849
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user