Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/self-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)"
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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._/+-]+`.
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -166,4 +180,5 @@ Tag workflow example with explicit source branch:
with:
coverage: "91%"
branch: "release/1.x"
token: ${{ github.token }}
```
40 changes: 40 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading