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
13 changes: 13 additions & 0 deletions actions/resolve-ref/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>`). 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.
11 changes: 11 additions & 0 deletions bin/resolve_ref.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name> 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
5 changes: 4 additions & 1 deletion docs/QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 49 additions & 0 deletions tests/test_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>`, 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(
Expand Down
Loading