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
232 changes: 232 additions & 0 deletions server/forge/github/fixtures/HookPushForced.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
{
"ref": "refs/heads/main",
"before": "5ea737297f2e2e88f4cdf796e72cbcb12d40fd9c",
"after": "f7220f1f753260bf6f1c533357c70213e9fd4abe",
"repository": {
"id": 1256442359,
"node_id": "R_kgDOSuPJ9w",
"name": "oxiduct",
"full_name": "6543/oxiduct",
"private": false,
"owner": {
"name": "6543",
"email": "6543@obermui.de",
"login": "6543",
"id": 24977596,
"node_id": "MDQ6VXNlcjI0OTc3NTk2",
"avatar_url": "https://avatars.githubusercontent.com/u/24977596?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/6543",
"html_url": "https://github.com/6543",
"followers_url": "https://api.github.com/users/6543/followers",
"following_url": "https://api.github.com/users/6543/following{/other_user}",
"gists_url": "https://api.github.com/users/6543/gists{/gist_id}",
"starred_url": "https://api.github.com/users/6543/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/6543/subscriptions",
"organizations_url": "https://api.github.com/users/6543/orgs",
"repos_url": "https://api.github.com/users/6543/repos",
"events_url": "https://api.github.com/users/6543/events{/privacy}",
"received_events_url": "https://api.github.com/users/6543/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"html_url": "https://github.com/6543/oxiduct",
"description": "Pipe traffic through oxidized steel. [tcp proxy ;)]",
"fork": false,
"url": "https://api.github.com/repos/6543/oxiduct",
"forks_url": "https://api.github.com/repos/6543/oxiduct/forks",
"keys_url": "https://api.github.com/repos/6543/oxiduct/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/6543/oxiduct/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/6543/oxiduct/teams",
"hooks_url": "https://api.github.com/repos/6543/oxiduct/hooks",
"issue_events_url": "https://api.github.com/repos/6543/oxiduct/issues/events{/number}",
"events_url": "https://api.github.com/repos/6543/oxiduct/events",
"assignees_url": "https://api.github.com/repos/6543/oxiduct/assignees{/user}",
"branches_url": "https://api.github.com/repos/6543/oxiduct/branches{/branch}",
"tags_url": "https://api.github.com/repos/6543/oxiduct/tags",
"blobs_url": "https://api.github.com/repos/6543/oxiduct/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/6543/oxiduct/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/6543/oxiduct/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/6543/oxiduct/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/6543/oxiduct/statuses/{sha}",
"languages_url": "https://api.github.com/repos/6543/oxiduct/languages",
"stargazers_url": "https://api.github.com/repos/6543/oxiduct/stargazers",
"contributors_url": "https://api.github.com/repos/6543/oxiduct/contributors",
"subscribers_url": "https://api.github.com/repos/6543/oxiduct/subscribers",
"subscription_url": "https://api.github.com/repos/6543/oxiduct/subscription",
"commits_url": "https://api.github.com/repos/6543/oxiduct/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/6543/oxiduct/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/6543/oxiduct/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/6543/oxiduct/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/6543/oxiduct/contents/{+path}",
"compare_url": "https://api.github.com/repos/6543/oxiduct/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/6543/oxiduct/merges",
"archive_url": "https://api.github.com/repos/6543/oxiduct/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/6543/oxiduct/downloads",
"issues_url": "https://api.github.com/repos/6543/oxiduct/issues{/number}",
"pulls_url": "https://api.github.com/repos/6543/oxiduct/pulls{/number}",
"milestones_url": "https://api.github.com/repos/6543/oxiduct/milestones{/number}",
"notifications_url": "https://api.github.com/repos/6543/oxiduct/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/6543/oxiduct/labels{/name}",
"releases_url": "https://api.github.com/repos/6543/oxiduct/releases{/id}",
"deployments_url": "https://api.github.com/repos/6543/oxiduct/deployments",
"created_at": 1780342272,
"updated_at": "2026-06-01T19:58:00Z",
"pushed_at": 1780343974,
"git_url": "git://github.com/6543/oxiduct.git",
"ssh_url": "git@github.com:6543/oxiduct.git",
"clone_url": "https://github.com/6543/oxiduct.git",
"svn_url": "https://github.com/6543/oxiduct",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": "Rust",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"has_discussions": false,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": {
"key": "mit",
"name": "MIT License",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit",
"node_id": "MDc6TGljZW5zZTEz"
},
"allow_forking": true,
"is_template": false,
"web_commit_signoff_required": false,
"has_pull_requests": true,
"pull_request_creation_policy": "all",
"topics": [],
"visibility": "public",
"forks": 0,
"open_issues": 0,
"watchers": 0,
"default_branch": "main",
"stargazers": 0,
"master_branch": "main"
},
"pusher": {
"name": "6543",
"email": "6543@obermui.de"
},
"forced": true,
"sender": {
"login": "6543",
"id": 24977596,
"node_id": "MDQ6VXNlcjI0OTc3NTk2",
"avatar_url": "https://avatars.githubusercontent.com/u/24977596?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/6543",
"html_url": "https://github.com/6543",
"followers_url": "https://api.github.com/users/6543/followers",
"following_url": "https://api.github.com/users/6543/following{/other_user}",
"gists_url": "https://api.github.com/users/6543/gists{/gist_id}",
"starred_url": "https://api.github.com/users/6543/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/6543/subscriptions",
"organizations_url": "https://api.github.com/users/6543/orgs",
"repos_url": "https://api.github.com/users/6543/repos",
"events_url": "https://api.github.com/users/6543/events{/privacy}",
"received_events_url": "https://api.github.com/users/6543/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"created": false,
"deleted": false,
"base_ref": null,
"compare": "https://github.com/6543/oxiduct/compare/5ea737297f2e...f7220f1f7532",
"commits": [
{
"id": "f7220f1f753260bf6f1c533357c70213e9fd4abe",
"tree_id": "c1f2cf4d1c9e83a01b8335b29f8d0f60eadb05db",
"distinct": true,
"message": "feat: initial implementation\n\nTCP/UDP proxy skeleton with layered dead-connection detection.\n\nCo-authored-by: Claude <noreply@anthropic.com>",
"timestamp": "2026-06-01T21:59:22+02:00",
"url": "https://github.com/6543/oxiduct/commit/f7220f1f753260bf6f1c533357c70213e9fd4abe",
"author": {
"name": "6543",
"email": "6543@obermui.de",
"date": "2026-06-01T19:56:16Z",
"username": "6543"
},
"committer": {
"name": "6543",
"email": "6543@obermui.de",
"date": "2026-06-01T21:59:22+02:00",
"username": "6543"
},
"added": [
".clippy.toml",
".gitignore",
".woodpecker/build.yml",
".woodpecker/lint.yml",
"Cargo.lock",
"Cargo.toml",
"LICENSE",
"README.md",
"contrib/example.toml",
"rustfmt.toml",
"src/cli.rs",
"src/config.rs",
"src/main.rs",
"src/proxy/mod.rs",
"src/proxy/tcp.rs",
"src/proxy/udp.rs",
"src/socket_opts.rs"
],
"removed": [],
"modified": []
}
],
"head_commit": {
"id": "f7220f1f753260bf6f1c533357c70213e9fd4abe",
"tree_id": "c1f2cf4d1c9e83a01b8335b29f8d0f60eadb05db",
"distinct": true,
"message": "feat: initial implementation\n\nTCP/UDP proxy skeleton with layered dead-connection detection.\n\nCo-authored-by: Claude <noreply@anthropic.com>",
"timestamp": "2026-06-01T21:59:22+02:00",
"url": "https://github.com/6543/oxiduct/commit/f7220f1f753260bf6f1c533357c70213e9fd4abe",
"author": {
"name": "6543",
"email": "6543@obermui.de",
"date": "2026-06-01T19:56:16Z",
"username": "6543"
},
"committer": {
"name": "6543",
"email": "6543@obermui.de",
"date": "2026-06-01T21:59:22+02:00",
"username": "6543"
},
"added": [
".clippy.toml",
".gitignore",
".woodpecker/build.yml",
".woodpecker/lint.yml",
"Cargo.lock",
"Cargo.toml",
"LICENSE",
"README.md",
"contrib/example.toml",
"rustfmt.toml",
"src/cli.rs",
"src/config.rs",
"src/main.rs",
"src/proxy/mod.rs",
"src/proxy/tcp.rs",
"src/proxy/udp.rs",
"src/socket_opts.rs"
],
"removed": [],
"modified": []
}
}
7 changes: 7 additions & 0 deletions server/forge/github/fixtures/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ import _ "embed"
//go:embed HookPush.json
var HookPush string

// HookPushForced is a sample push hook from a force push. The "before" commit
// is unreachable after the history rewrite, so it must not be used to compare.
// https://developer.github.com/v3/activity/events/types/#pushevent
//
//go:embed HookPushForced.json
var HookPushForced string

// HookPushDeleted is a sample push hook that is marked as deleted, and is expected to be ignored.
const HookPushDeleted = `
{
Expand Down
3 changes: 1 addition & 2 deletions server/forge/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,8 +819,7 @@ func (c *client) loadChangedFilesFromCommits(ctx context.Context, tmpRepo *model
prev = ""
fallthrough
case "":
// For tag events, prev is empty, but we can still fetch the changed files using the current commit
log.Trace().Msg("GitHub tag event, fetching changed files using current commit")
log.Trace().Msg("GitHub force push or tag event, fetching changed files using current commit")
}

repo, err := _store.GetRepoNameFallback(c.id, tmpRepo.ForgeRemoteID, tmpRepo.FullName)
Expand Down
6 changes: 6 additions & 0 deletions server/forge/github/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"strings"

"github.com/google/go-github/v88/github"
"github.com/rs/zerolog/log"

"go.woodpecker-ci.org/woodpecker/v3/server/forge/common"
"go.woodpecker-ci.org/woodpecker/v3/server/forge/types"
Expand Down Expand Up @@ -126,6 +127,11 @@ func parsePushHook(hook *github.PushEvent) (_ *model.Repo, _ *model.Pipeline, cu
return repo, pipeline, "", ""
}

if hook.GetForced() {
log.Debug().Msgf("detected force push on push hook [%d]", hook.GetPushID())
return repo, pipeline, hook.GetHeadCommit().GetID(), ""
}

return repo, pipeline, hook.GetHeadCommit().GetID(), hook.GetBefore()
}

Expand Down
16 changes: 16 additions & 0 deletions server/forge/github/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ func Test_parseHook(t *testing.T) {
sort.Strings(b.ChangedFiles)
})

t.Run("forced push hook", func(t *testing.T) {
// On a force push the "before" commit may be unreachable after the
// history rewrite. Comparing against it (CompareCommits) fails with a
// 404 on the forge, so the previous commit must be empty to make the
// downstream changed-files lookup fall back to the head commit only.
req := testHookRequest([]byte(fixtures.HookPushForced), hookPush)
p, r, b, cc, pc, err := parseHook(req, false)
assert.NoError(t, err)
assert.Equal(t, "f7220f1f753260bf6f1c533357c70213e9fd4abe", cc)
assert.Empty(t, pc, "forced push must not expose the unreachable before commit")
assert.Nil(t, p)
assert.NotNil(t, r)
assert.NotNil(t, b)
assert.Equal(t, model.EventPush, b.Event)
})

t.Run("PR hook", func(t *testing.T) {
req := testHookRequest([]byte(fixtures.HookPullRequest), hookPull)
p, r, b, cc, pc, err := parseHook(req, false)
Expand Down