diff --git a/actions/resolve-ref/README.md b/actions/resolve-ref/README.md index 1188c61..a99984c 100644 --- a/actions/resolve-ref/README.md +++ b/actions/resolve-ref/README.md @@ -82,3 +82,16 @@ So this does not say the job is unprivileged. A `workflow_dispatch` workflow that granted `contents: write` and passed a secret would pass it. That part is the workflow's to declare, and a caller should write `permissions: contents: read` at the top of any workflow that builds a ref. + +## In a private repository + +The workflows check out with `persist-credentials: false`, so the token is not +left in `.git/config` for the build to find. That also means a later +`git fetch` has nothing to authenticate with. A public repository answers it +anyway and a private one refuses it. + +So in a private repository a ref resolves only if the checkout already has it: +a tag, a commit on a branch or a tag, or a branch (the one the workflow runs +on, or any other, which a full checkout keeps as `origin/`). A pull +request number does not, because its head is under `refs/pull/` and a checkout +does not fetch that. Nor does a commit that is on no branch or tag. diff --git a/bin/resolve_ref.sh b/bin/resolve_ref.sh index ae30661..af155a8 100755 --- a/bin/resolve_ref.sh +++ b/bin/resolve_ref.sh @@ -44,5 +44,16 @@ if sha=$(git rev-parse --verify --quiet "${ref}^{commit}"); then exit 0 fi +# A branch other than the one the workflow runs on. A full checkout keeps every +# branch, but as origin/ rather than under its own name, so the line +# above misses it. Fetching it by name asks the remote again, which a private +# repository refuses: the checkout keeps no credentials to ask with. Anything +# still missing after this (a pull request head, a commit on no branch) has to +# be fetched. +if sha=$(git rev-parse --verify --quiet "refs/remotes/origin/${ref}^{commit}"); then + echo "$sha" + exit 0 +fi + git fetch -q origin "$ref" git rev-parse FETCH_HEAD diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 2a69aef..8192fc2 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -13,7 +13,10 @@ runners and puts the result in the run summary. - if the repository belongs to an organisation, its Actions settings have to allow actions and reusable workflows from other repositories -So far mache has only been run from public repositories. +So far mache has only been run from public repositories. In a private one, +stick to tags, commits and the branch you run from for now: +[actions/resolve-ref](../actions/resolve-ref/README.md#in-a-private-repository) +has why. ## 1. Build script diff --git a/tests/test_resolve.py b/tests/test_resolve.py index 59c75bd..108a53f 100644 --- a/tests/test_resolve.py +++ b/tests/test_resolve.py @@ -146,6 +146,55 @@ def test_a_ref_that_is_not_there_fails(self, repository): assert resolve(repository, "v9.9.9").returncode != 0 +class TestAPrivateRepository: + """What resolves without asking the remote again. + + The workflows check out with `persist-credentials: false`, so a later + fetch has no credentials. A public repository answers anyway and a private + one refuses. These tests clone the fixture the way a full checkout does + (every branch as `origin/`, every tag, one branch checked out) and + then point `origin` somewhere nothing can be fetched from, which is where + a private repository leaves a job.""" + + @pytest.fixture + def checkout(self, repository, tmp_path): + remote, shas = repository + git(remote, "branch", "feature", shas["second"]) + path = tmp_path / "checkout" + path.mkdir() + git(path, "init", "-q") + git(path, "remote", "add", "origin", str(remote)) + git( + path, + "fetch", + "-q", + "origin", + "+refs/heads/*:refs/remotes/origin/*", + "+refs/tags/*:refs/tags/*", + ) + git(path, "checkout", "-q", "-b", "main", "origin/main") + git(path, "remote", "set-url", "origin", str(tmp_path / "nowhere")) + return path, shas + + def test_a_branch_that_is_not_checked_out_resolves(self, checkout): + name, sha = lines(resolve(checkout, "feature")) + assert name == "feature" + assert sha == checkout[1]["second"] + + def test_a_tag_and_a_commit_still_resolve(self, checkout): + _, shas = checkout + assert lines(resolve(checkout, "v0.1.0"))[1] == shas["first"] + assert lines(resolve(checkout, shas["third"]))[1] == shas["third"] + + def test_a_pull_request_still_has_to_be_fetched(self, checkout): + # its head is under refs/pull, which a checkout does not fetch + result = resolve(checkout, "7") + assert result.returncode != 0 + + def test_a_branch_nobody_pushed_still_fails(self, checkout): + assert resolve(checkout, "no-such-branch").returncode != 0 + + class TestAnEmptyRef: def test_head_is_the_commit_the_workflow_runs_on(self, repository): name, sha = lines(