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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Flags follow act's names (`--matrix`, `--reuse`, `--json`, `--input-file`, …);
| Event payload | `-e event.json` | — |
| Default arguments | — | `$XDG_CONFIG_HOME/act/actrc`, `~/.actrc`, `./.actrc` |

Flags win over files. Arguments from `.actrc` files are read before the command line's. Without `-e`, a `merge_group` run gets a payload built from the repository (`head_sha`/`head_ref` from HEAD, `base_sha` where HEAD forked from the remote's default branch).
Flags win over files. Arguments from `.actrc` files are read before the command line's. Without `-e`, a `merge_group` run gets a payload built from the repository (`head_sha`/`head_ref` from HEAD, `base_sha` where HEAD forked from the remote's default branch). A `pull_request` or `pull_request_target` run gets a PR from the current branch into the default branch (`github.base_ref`/`head_ref`; `--defaultbranch` picks another base).

### Dashboard

Expand Down
63 changes: 63 additions & 0 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,33 @@ impl RepoInfo {
})
}

/// A pull_request payload for HEAD when no event file is given: a PR from
/// the current branch (`head`) into the default branch (`base`), so
/// `github.base_ref`/`head_ref` are set. `base.sha` is `<remote>/<default>`,
/// or the local `<default>` without it, unless it is HEAD itself, where
/// `on.pull_request.paths` would see no changes. No `action`, so every
/// `types` filter passes.
pub fn pull_request_event(&self, remote: &str, default_branch: &str) -> Value {
let mut event = json!({
"pull_request": {
"head": {"ref": self.ref_name, "sha": self.sha},
"base": {"ref": default_branch},
},
});
let commit = |r: String| {
git(
&self.root,
&["rev-parse", "--verify", "-q", &format!("{r}^{{commit}}")],
)
};
let base_sha = commit(format!("refs/remotes/{remote}/{default_branch}"))
.or_else(|| commit(format!("refs/heads/{default_branch}")));
if let Some(sha) = base_sha.filter(|s| *s != self.sha) {
event["pull_request"]["base"]["sha"] = json!(sha);
}
event
}

/// `<remote>/<branch>` when HEAD is ahead of it, as GitHub's `before` for a
/// push of HEAD; `None` for a new, behind or up-to-date branch.
fn push_before(&self, remote: &str) -> Option<String> {
Expand Down Expand Up @@ -485,6 +512,42 @@ mod tests {
assert_eq!(repo.push_before("origin"), None);
}

#[test]
fn pull_request_base_sha_is_the_remote_or_local_default_branch() {
let dir = tempfile::tempdir().unwrap();
let run = |args: &[&str]| {
let ok = Command::new("git")
.arg("-C")
.arg(dir.path())
.args(["-c", "user.name=t", "-c", "user.email=t@x"])
.args(args)
.status()
.unwrap()
.success();
assert!(ok, "git {args:?}");
};
let base_sha = |remote: &str| {
let repo = RepoInfo::detect(dir.path(), remote);
repo.pull_request_event(remote, "main")["pull_request"]["base"]["sha"].clone()
};
run(&["init", "-q", "-b", "main"]);
run(&["commit", "-q", "--allow-empty", "-m", "a"]);
let a = git(dir.path(), &["rev-parse", "HEAD"]).unwrap();
// On the default branch itself: no base.sha.
assert_eq!(base_sha("origin"), Value::Null);
run(&["checkout", "-q", "-b", "topic"]);
run(&["commit", "-q", "--allow-empty", "-m", "b"]);
// No remote-tracking ref: the local default branch.
assert_eq!(base_sha("origin"), a.as_str());
run(&["update-ref", "refs/remotes/origin/main", "topic"]);
run(&["commit", "-q", "--allow-empty", "-m", "c"]);
let b = git(dir.path(), &["rev-parse", "HEAD~1"]).unwrap();
// The remote-tracking ref wins over the local branch.
assert_eq!(base_sha("origin"), b.as_str());
let event = RepoInfo::detect(dir.path(), "origin").pull_request_event("origin", "main");
assert_eq!(event["pull_request"]["head"]["ref"], "topic");
}

#[test]
fn slugs() {
assert_eq!(
Expand Down
10 changes: 8 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,14 @@ fn run(cli: &Cli) -> Result<bool> {
.defaultbranch
.clone()
.unwrap_or_else(|| repo.default_branch.clone());
if cli.eventpath.is_none() && event_name == "merge_group" {
event = repo.merge_group_event(&cli.remote_name, &branch);
if cli.eventpath.is_none() {
match event_name.as_str() {
"merge_group" => event = repo.merge_group_event(&cli.remote_name, &branch),
"pull_request" | "pull_request_target" => {
event = repo.pull_request_event(&cli.remote_name, &branch)
}
_ => {}
}
}
repo.fill_event_repository(&mut event, &branch);

Expand Down
4 changes: 3 additions & 1 deletion tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,11 @@ fn runthrough_dir_workflows_are_discovered() {

#[test]
fn failing_workflow_runs_failure_steps_and_skips_dependents() {
let (code, out) = runthrough(&["pull_request"]);
let (code, out) = runthrough(&["pull_request", "--defaultbranch", "trunk"]);
assert_eq!(code, 1, "{out}");
assert!(out.contains("node got ab"), "{out}");
// Without `-e`, the payload is a PR from HEAD into the default branch.
assert!(out.contains("pr base_ref=trunk head_ref=true"), "{out}");
assert!(out.contains("cleanup after failure"), "{out}");
assert!(out.contains("post sees state ab"), "{out}");
assert!(out.contains("failing / never: skipped"), "{out}");
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/repo/.github/workflows/failing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jobs:
with:
value: ab
- run: test "${{ steps.js.outputs.doubled }}" = abab
- run: echo "pr base_ref=${{ github.base_ref }} head_ref=${{ github.head_ref != '' }}"
- run: exit 3
- if: failure()
run: echo "cleanup after failure"
Expand Down
Loading