Skip to content

Commit adeeb59

Browse files
committed
Dedup AppVeyor builds
1 parent e4bd470 commit adeeb59

File tree

5 files changed

+147
-4
lines changed

5 files changed

+147
-4
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Features
1616
- [Send contributing advice message](#hello-contributor)
1717
- [Auto-label PRs](#auto-labelling)
1818
- [Using the Dlang-Bot for your project](#dlang-bot-for-your-project)
19+
- [Canceling stale AppVeyor builds](#canceling-stale)
1920
- [Missing a feature?](#missing-a-feature)
2021

2122
<a name="automated-references" />
@@ -207,6 +208,15 @@ For example, `dlang/phobos` is configured as follows:
207208

208209
![image](https://user-images.githubusercontent.com/4370550/27859920-b418a38e-617a-11e7-9ff2-c1fd9f6fdd20.png)
209210

211+
<a name="#canceling-stale" />
212+
213+
Canceling stale AppVeyor builds
214+
-------------------------------
215+
216+
To avoid wasteful resource consumption with Travis CI,
217+
the Dlang-Bot will automatically cancel the previous, possibly running build of
218+
a PR on a new commit event (push or synchronization by a user).
219+
210220
<a name="missing-a-feature" />
211221

212222
Missing a feature?

source/dlangbot/app.d

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
module dlangbot.app;
22

3-
import dlangbot.bugzilla, dlangbot.cron, dlangbot.github, dlangbot.trello,
4-
dlangbot.utils;
5-
3+
import dlangbot.appveyor;
4+
import dlangbot.bugzilla;
5+
import dlangbot.cron;
6+
import dlangbot.github;
7+
import dlangbot.trello;
8+
import dlangbot.utils;
9+
10+
public import dlangbot.appveyor : appveyorAPIURL, appveyorAuth;
611
public import dlangbot.bugzilla : bugzillaURL;
712
public import dlangbot.github_api : githubAPIURL, githubAuth, hookSecret;
813
public import dlangbot.trello : trelloAPIURL, trelloAuth, trelloSecret;
@@ -21,6 +26,7 @@ import vibe.stream.operations : readAllUTF8;
2126

2227
bool runAsync = true;
2328
bool runTrello = true;
29+
bool runAppVeyor = true;
2430

2531
Duration timeBetweenFullPRChecks = 1.minutes; // this should never be larger 30 mins on heroku
2632
Throttler!(typeof(&searchForAutoMergePrs)) prThrottler;
@@ -230,6 +236,24 @@ void handlePR(string action, PullRequest* _pr)
230236
logDebug("[github/handlePR](%s): updating trello card", _pr.pid);
231237
updateTrelloCard(action, pr.htmlURL, refs, descs);
232238
}
239+
240+
241+
// wait until builds for the current push are created
242+
if (pr.repoSlug == "dlang/dmd" && runAppVeyor)
243+
{
244+
// AppVeyor doesn't support organizations natively
245+
import std.array : replace;
246+
auto repoSlug = pr.repoSlug.replace("dlang/dmd", "greenify/dmd");
247+
setBotTimer(30.seconds, { dedupAppVeyorBuilds(action, repoSlug, pr.number); });
248+
}
249+
}
250+
251+
void setBotTimer(C)(Duration dur, C callback)
252+
{
253+
if (runAsync)
254+
setTimer(dur, callback);
255+
else
256+
callback();
233257
}
234258

235259
//==============================================================================
@@ -254,6 +278,7 @@ else void main(string[] args)
254278
trelloSecret = environment["TRELLO_SECRET"];
255279
trelloAuth = "key="~environment["TRELLO_KEY"]~"&token="~environment["TRELLO_TOKEN"];
256280
hookSecret = environment["GH_HOOK_SECRET"];
281+
appveyorAuth = "Bearer " ~ environment["APPVEYOR_TOKEN"];
257282

258283
// workaround for stupid openssl.conf on Heroku
259284
if (environment.get("DYNO") !is null)

source/dlangbot/appveyor.d

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
module dlangbot.appveyor;
2+
3+
string appveyorAPIURL = "https://ci.appveyor.com/api";
4+
string appveyorAuth;
5+
6+
import vibe.core.log;
7+
8+
//==============================================================================
9+
// Dedup AppVeyor builds
10+
//==============================================================================
11+
12+
// https://www.appveyor.com/docs/api/projects-builds/#cancel-build
13+
void cancelBuild(string repoSlug, size_t buildId)
14+
{
15+
import std.format : format;
16+
import vibe.http.client : requestHTTP;
17+
import vibe.http.common : HTTPMethod;
18+
import vibe.stream.operations : readAllUTF8;
19+
20+
auto url = "%s/builds/%s/%s/cancel".format(appveyorAPIURL, repoSlug, buildId);
21+
requestHTTP(url, (scope req) {
22+
req.headers["Authorization"] = appveyorAuth;
23+
req.method = HTTPMethod.DELETE;
24+
}, (scope res) {
25+
if (res.statusCode / 100 == 2)
26+
logInfo("[appveyor/%s]: Canceled Build %s\n", repoSlug, buildId);
27+
else
28+
logWarn("[appveyor/%s]: POST %s failed; %s %s.\n%s", repoSlug, url, res.statusPhrase,
29+
res.statusCode, res.bodyReader.readAllUTF8);
30+
});
31+
}
32+
33+
// https://www.appveyor.com/docs/api/projects-builds/#get-project-history
34+
void dedupAppVeyorBuilds(string action, string repoSlug, uint pullRequestNumber)
35+
{
36+
import std.algorithm.iteration : filter;
37+
import std.conv : to;
38+
import std.format : format;
39+
import std.range : drop;
40+
import vibe.data.json : Json;
41+
import vibe.http.client : requestHTTP;
42+
43+
if (action != "synchronize" && action != "merged")
44+
return;
45+
46+
static bool activeState(string state)
47+
{
48+
import std.algorithm.comparison : among;
49+
return state.among("created", "queued", "started") > 0;
50+
}
51+
// GET /api/projects/{accountName}/{projectSlug}/history?recordsNumber={records-per-page}[&startBuildId={buildId}&branch={branch}]
52+
53+
auto url = "%s/projects/%s/history?recordsNumber=100".format(appveyorAPIURL, repoSlug);
54+
auto activeBuildsForPR = requestHTTP(url, (scope req) {
55+
req.headers["Authorization"] = appveyorAuth;
56+
})
57+
.readJson["builds"][]
58+
.filter!(b => "pullRequestId" in b && !b["pullRequestId"].type != Json.undefined)
59+
.filter!(b => activeState(b["status"].get!string))
60+
.filter!(b => b["pullRequestId"].get!string.to!uint == pullRequestNumber);
61+
62+
// Keep only the most recent build for this PR. Kill all builds
63+
// when it got merged as it'll be retested after the merge anyhow.
64+
foreach (b; activeBuildsForPR.drop(action == "merged" ? 0 : 1))
65+
cancelBuild(repoSlug, b["buildId"].get!size_t);
66+
}
67+

test/appveyor.d

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import utils;
2+
3+
unittest
4+
{
5+
setAPIExpectations(
6+
"/github/repos/dlang/dmd/pulls/6359/commits", (ref Json j) {
7+
j = Json.emptyArray;
8+
},
9+
"/github/repos/dlang/dmd/issues/6359/comments",
10+
"/github/orgs/dlang/public_members?per_page=100",
11+
"/github/repos/dlang/dmd/issues/6359/comments", // dlang-bot post
12+
"/github/repos/dlang/dmd/issues/6359/labels",
13+
"/appveyor/projects/greenify/dmd/history?recordsNumber=100", (ref Json j) {
14+
j["builds"][0]["status"] = "queued";
15+
j["builds"][1]["status"] = "queued";
16+
j["builds"][2]["status"] = "cancelled";
17+
j["builds"][0]["pullRequestId"] = "6359";
18+
j["builds"][1]["pullRequestId"] = "6359";
19+
j["builds"][2]["pullRequestId"] = "6359";
20+
j["builds"] = j["builds"][0..2];
21+
},
22+
"/appveyor/builds/greenify/dmd/13074433/cancel",
23+
(scope HTTPServerRequest req, scope HTTPServerResponse res) {
24+
assert(req.method == HTTPMethod.DELETE);
25+
res.statusCode = 204;
26+
res.writeVoidBody;
27+
},
28+
);
29+
30+
runAppVeyor = true;
31+
scope(exit) runAppVeyor = false;
32+
postGitHubHook("dlang_phobos_synchronize_4921.json", "pull_request", (ref Json j, scope req){
33+
j["pull_request"]["number"] = 6359;
34+
j["pull_request"]["base"]["repo"]["name"] = "dmd";
35+
j["pull_request"]["base"]["repo"]["full_name"] = "dlang/dmd";
36+
j["pull_request"]["base"]["repo"]["owner"]["login"] = "dlang";
37+
});
38+
}

test/utils.d

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ shared static this()
4040
githubAuth = "GH_DUMMY_AUTH_TOKEN";
4141
hookSecret = "GH_DUMMY_HOOK_SECRET";
4242
trelloAuth = "key=01234&token=abcde";
43+
appveyorAuth = "key=01234&token=abcde";
4344

4445
// start our hook server
4546
auto settings = new HTTPServerSettings;
@@ -56,6 +57,7 @@ shared static this()
5657
setLogLevel(LogLevel.info);
5758

5859
runAsync = false;
60+
runAppVeyor = false;
5961
}
6062

6163
void startFakeAPIServer()
@@ -75,6 +77,7 @@ void startFakeAPIServer()
7577
githubAPIURL = fakeAPIServerURL ~ "/github";
7678
trelloAPIURL = fakeAPIServerURL ~ "/trello";
7779
bugzillaURL = fakeAPIServerURL ~ "/bugzilla";
80+
appveyorAPIURL = fakeAPIServerURL ~ "/appveyor";
7881
}
7982

8083
// serves saved GitHub API payloads
@@ -124,7 +127,7 @@ auto payloadServer(scope HTTPServerRequest req, scope HTTPServerResponse res)
124127
{
125128
logInfo("reading payload: %s", filePath);
126129
auto payload = filePath.readText;
127-
if (req.requestURL.startsWith("/github", "/trello"))
130+
if (req.requestURL.startsWith("/github", "/trello", "/appveyor"))
128131
{
129132
auto payloadJson = payload.parseJsonString;
130133
replaceAPIReferences("https://api.github.com", githubAPIURL, payloadJson);

0 commit comments

Comments
 (0)