diff --git a/.github/workflows/self-test.yml b/.github/workflows/self-test.yml index 22d8dba..058dfcc 100644 --- a/.github/workflows/self-test.yml +++ b/.github/workflows/self-test.yml @@ -34,6 +34,45 @@ jobs: with: path: action-src + - name: Credential helper returns x-access-token (unit) + shell: bash + run: | + set -euo pipefail + # The fixture below uses a local file-based remote that never invokes a + # credential helper, so this is the only place the HTTPS helper output is + # exercised directly. The helper string MUST stay byte-identical to the one + # configured by the 'Configure Git credential helper' step in action.yml. + export GIT_CONFIG_NOSYSTEM=1 + export GIT_TERMINAL_PROMPT=0 + TMP_CFG="$(mktemp)" + export GIT_CONFIG_GLOBAL="$TMP_CFG" + # The helper command is stored literally; $GITCOVERAGE_GIT_TOKEN is expanded + # by the helper at git-invocation time, not here. + # shellcheck disable=SC2016 + git config --global 'credential.https://github.com.helper' \ + '!f() { if test "$1" = get && test -n "${GITCOVERAGE_GIT_TOKEN}"; then echo "username=x-access-token"; echo "password=${GITCOVERAGE_GIT_TOKEN}"; fi; }; f' + + # The token value must never be written into the config file. + if grep -q 'unit-test-token-123' "$TMP_CFG"; then + echo "token value leaked into git config" >&2 + exit 1 + fi + + # Token present: helper returns x-access-token and the token as password. + # 'git credential fill' requires the trailing blank line; use printf for LF. + out="$(printf 'protocol=https\nhost=github.com\n\n' | env GITCOVERAGE_GIT_TOKEN=unit-test-token-123 git credential fill)" + printf '%s\n' "$out" | grep -q '^username=x-access-token$' + printf '%s\n' "$out" | grep -q '^password=unit-test-token-123$' + + # Empty token: helper emits nothing, so no credentials are returned. + empty_out="$(printf 'protocol=https\nhost=github.com\n\n' | env GITCOVERAGE_GIT_TOKEN= git credential fill 2>/dev/null || true)" + if printf '%s\n' "$empty_out" | grep -q '^password='; then + echo "credential helper leaked a password with an empty token" >&2 + exit 1 + fi + rm -f "$TMP_CFG" + echo "credential helper unit test passed" + - name: Prepare local fixture repository shell: bash run: | @@ -134,6 +173,20 @@ jobs: echo "$files" | grep -Fx "main/badge.svg" echo "$files" | grep -Fx "main/report.html" + - name: Verify no credential helper configured without token + shell: bash + run: | + set -euo pipefail + # The runs so far passed no 'token', so the action must not have registered + # a credential helper. Read the action's global config (the action exports + # GIT_CONFIG_GLOBAL via GITHUB_ENV when it uses an isolated config). Check the + # scoped key only, to tolerate any helper a runner may preconfigure. + SERVER_URL="${GITHUB_SERVER_URL:-https://github.com}" + SERVER_URL="${SERVER_URL%/}" + helpers="$(git config --global --get-all "credential.${SERVER_URL}.helper" || true)" + test -z "$helpers" + echo "no credential helper configured without token (verified)" + - name: Run action in tag-triggered mode uses: ./action-src env: @@ -173,3 +226,44 @@ jobs: run: | set -euo pipefail test "${{ steps.detached_tag.outcome }}" = "failure" + + - name: Run action with token (regression) + uses: ./action-src + with: + coverage: "75%" + branch: main + token: ${{ github.token }} + + - name: Verify credential helper cleared after token run + shell: bash + run: | + set -euo pipefail + # Pushes here target the local file remote, which never invokes the helper, + # so this proves the token input does not regress the push and that the + # always() cleanup removed the helper afterwards. + SERVER_URL="${GITHUB_SERVER_URL:-https://github.com}" + SERVER_URL="${SERVER_URL%/}" + helpers="$(git config --global --get-all "credential.${SERVER_URL}.helper" || true)" + test -z "$helpers" + echo "credential helper cleared after successful token run (verified)" + + - name: Run action with token on invalid branch (should fail) + id: token_failure + continue-on-error: true + uses: ./action-src + with: + coverage: "70%" + branch: "partial" + token: ${{ github.token }} + + - name: Verify failed token run still cleared credential helper + shell: bash + run: | + set -euo pipefail + test "${{ steps.token_failure.outcome }}" = "failure" + # The always() cleanup must run even when an earlier step fails. + SERVER_URL="${GITHUB_SERVER_URL:-https://github.com}" + SERVER_URL="${SERVER_URL%/}" + helpers="$(git config --global --get-all "credential.${SERVER_URL}.helper" || true)" + test -z "$helpers" + echo "credential helper cleared after failed token run (verified)" diff --git a/README.md b/README.md index 097f9a1..7d4a559 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ You need to have given write permissions for the for the workflow job that runs If the 'gitcoverage' branch does not exist, it will be created as an orphan (without main repo history). The action creates bot commits with signing disabled (`commit.gpgsign=false`) for compatibility with runners that enforce local signing config but have no key. If your `gitcoverage` branch requires signed commits, configure signing keys on the runner or relax that branch rule. +By default the action pushes using the credentials that `actions/checkout` persists. +Pass the `token` input to authenticate explicitly instead, which lets you check out with `persist-credentials: false`. +When credentials are still persisted (`persist-credentials: true`), those take precedence over the `token` input. Reference the generated badge in your README.md like this: ```md @@ -32,6 +35,9 @@ If you submitted a detailed HTML report of the coverage to the action, replace t - `coverage` (required): Coverage percentage (for example `83` or `83%`). - `report` (optional): Path to an HTML report file to publish as `report.html`. +- `token` (optional): GitHub token used to push updates to the `gitcoverage` branch. + When set, the action configures a temporary Git credential helper, so you can check out with `actions/checkout` and `persist-credentials: false`. + When omitted, the action uses the credentials persisted by `actions/checkout`. - `branch` (optional): Source branch override. Recommended for tag-triggered workflows where multiple branches may contain the same tag commit. Also recommended for very large or restricted repos to avoid scanning all remote branches during tag-triggered branch resolution. On Windows runners, the action applies a strict compatibility filter and requires branch names to match `[A-Za-z0-9._/+-]+`. @@ -52,10 +58,13 @@ jobs: contents: write steps: - uses: actions/checkout@v6 + with: + persist-credentials: false - uses: linkdata/gitcoverage@v9 with: coverage: "83%" report: "coveragereport.html.out" + token: ${{ github.token }} ``` More complete example using Go: @@ -81,6 +90,8 @@ jobs: coverage: ${{ steps.coverage.outputs.coverage }} steps: - uses: actions/checkout@v6 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@v6 @@ -145,6 +156,8 @@ jobs: contents: write steps: - uses: actions/checkout@v6 + with: + persist-credentials: false - name: Download code coverage uses: actions/download-artifact@v8 @@ -156,6 +169,7 @@ jobs: with: coverage: ${{ needs.build.outputs.coverage }} report: "coveragereport.html.out" + token: ${{ github.token }} ``` Tag workflow example with explicit source branch: @@ -166,4 +180,5 @@ Tag workflow example with explicit source branch: with: coverage: "91%" branch: "release/1.x" + token: ${{ github.token }} ``` diff --git a/action.yml b/action.yml index cc8832b..4727f3a 100644 --- a/action.yml +++ b/action.yml @@ -28,6 +28,9 @@ inputs: report: description: "Optional path to an HTML coverage report file to publish as report.html" required: false + token: + description: "GitHub token used to push updates to the gitcoverage branch" + required: false runs: using: "composite" steps: @@ -81,6 +84,23 @@ runs: git config --local user.email "action@github.com" git config --local user.name "GitHub Action" + - name: Configure Git credential helper (when token provided) + if: ${{ inputs.token != '' }} + shell: bash + run: | + set -euo pipefail + # Store ONLY a helper command that reads the token from the environment. + # The token value is never written to git config; the helper expands + # $GITCOVERAGE_GIT_TOKEN at git-invocation time (see the network-op steps). + # The helper self-disables when the variable is empty so git falls back to + # any credentials persisted by actions/checkout (or anonymous access). + # Relies on the GIT_CONFIG_GLOBAL exported by the 'Sanity / git identity' step. + SERVER_URL="${GITHUB_SERVER_URL:-https://github.com}" + SERVER_URL="${SERVER_URL%/}" + # shellcheck disable=SC2016 # helper command stored literally; expanded by git at invocation time + git config --global "credential.${SERVER_URL}.helper" \ + '!f() { if test "$1" = get && test -n "${GITCOVERAGE_GIT_TOKEN}"; then echo "username=x-access-token"; echo "password=${GITCOVERAGE_GIT_TOKEN}"; fi; }; f' + - name: Detect current branch (handles tags; fetches tags; robust) id: branch shell: bash @@ -91,6 +111,8 @@ runs: CTX_REF_TYPE: ${{ env.GITCOVERAGE_REF_TYPE }} CTX_REF_NAME: ${{ env.GITCOVERAGE_REF_NAME }} CTX_REF: ${{ env.GITCOVERAGE_REF }} + # Network-op step: carry the token so the credential helper can authenticate. + GITCOVERAGE_GIT_TOKEN: ${{ inputs.token }} run: | set -euo pipefail @@ -289,6 +311,10 @@ runs: - name: Ensure 'gitcoverage' branch exists (create orphan if needed) shell: bash + env: + # Network-op step: carry the token so the credential helper can authenticate + # the orphan-branch push and the ls-remote/fetch calls in this step. + GITCOVERAGE_GIT_TOKEN: ${{ inputs.token }} run: | set -euo pipefail to_posix_path() { @@ -553,6 +579,9 @@ runs: - name: Commit & push changes to gitcoverage branch env: BRANCH: ${{ steps.branch.outputs.branch }} + # Network-op step: carry the token so the credential helper can authenticate + # the push and the rebase-retry fetch in this step. + GITCOVERAGE_GIT_TOKEN: ${{ inputs.token }} shell: bash run: | set -euo pipefail @@ -616,3 +645,14 @@ runs: rm -rf -- "$WORKTREE_DIR" || true fi git worktree prune || true + + - name: Clear Git credential helper + if: ${{ always() && inputs.token != '' }} + shell: bash + run: | + set -euo pipefail + # Remove the helper registered by 'Configure Git credential helper' + # (scoped key only). Tolerate the case where it was never set. + SERVER_URL="${GITHUB_SERVER_URL:-https://github.com}" + SERVER_URL="${SERVER_URL%/}" + git config --global --unset-all "credential.${SERVER_URL}.helper" || true