Skip to content

Commit 1290fd4

Browse files
committed
Build/deploy pipeline using GitHub actions
1 parent 75425f5 commit 1290fd4

File tree

4 files changed

+257
-0
lines changed

4 files changed

+257
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Build and Trigger Deploy
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
create:
8+
tags:
9+
- 'v*'
10+
11+
env:
12+
IMAGEID: rapidlua/luajit.me
13+
PACKERV: 1.5.1
14+
15+
jobs:
16+
17+
cloud-images:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Install packer
21+
run: >
22+
curl https://releases.hashicorp.com/packer/${PACKERV}/packer_${PACKERV}_linux_$(dpkg --print-architecture).zip > packer.zip &&
23+
unzip packer.zip &&
24+
sudo install packer /usr/bin &&
25+
rm packer.zip packer
26+
- uses: actions/checkout@v1
27+
- name: Build cloud images
28+
run: >
29+
VERSION=$(git describe --tags | sed -e s/^v//);
30+
packer build -var "version=${VERSION}" -var "digitalocean_token=${{ secrets.DIGITALOCEAN_TOKEN }}" deploy/cloud-images.json
31+
32+
docker-image-amd64:
33+
runs-on: ubuntu-latest
34+
steps:
35+
- uses: actions/checkout@v1
36+
- name: Build Docker image
37+
run: >
38+
VERSION=$(git describe --tags | sed -e s/^v//);
39+
mkdir -p build &&
40+
(cd app && DOCKER_BUILDKIT=1 docker build . --tag "${IMAGEID}:${VERSION}-amd64") &&
41+
docker save "${IMAGEID}:${VERSION}-amd64" | gzip > build/image.tar.gz &&
42+
echo "$VERSION" > build/version
43+
- uses: actions/upload-artifact@v1.0.0
44+
with: { name: docker-image-amd64, path: build }
45+
46+
postprocess-and-trigger-deploy:
47+
runs-on: ubuntu-latest
48+
needs: [cloud-images, docker-image-amd64]
49+
steps:
50+
- uses: actions/download-artifact@v1.0.0
51+
with: { name: docker-image-amd64 }
52+
- name: Configure Docker client / login
53+
run: >
54+
mkdir -p ~/.docker &&
55+
echo '{"experimental":"enabled"}' > ~/.docker/config.json &&
56+
echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u mejedi --password-stdin
57+
- name: Upload Docker images
58+
run: >
59+
VERSION=$(cat docker-image-amd64/version);
60+
IID=${IMAGEID}:${VERSION};
61+
zcat docker-image-amd64/image.tar.gz | docker image load &&
62+
docker push "${IID}-amd64" &&
63+
docker manifest create "${IID}" "${IID}-amd64" &&
64+
docker manifest push "${IID}"
65+
- name: Trigger deploy
66+
run: >
67+
if cat docker-image-amd64/version | grep -q -- -g; then
68+
ENV=staging
69+
else
70+
ENV=production
71+
fi;
72+
curl -sd "{\"ref\":\"${GITHUB_SHA}\",\"required_contexts\":[],\"environment\":\"${ENV}\"}" https://api.github.com/repos/${GITHUB_REPOSITORY}/deployments -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}"

.github/workflows/gc-cloud-images.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
const URL = require('url').URL;
2+
const https = require('https');
3+
const child_process = require('child_process');
4+
5+
function apiCall(url, method, headers, cb) {
6+
const u = new URL(url);
7+
const req = https.request({ hostname: u.hostname, path: u.pathname + u.search, method, headers }, (res) => {
8+
if (res.statusCode !== 200 && res.statusCode !== 204) {
9+
cb(new Error(res.statusMessage));
10+
return;
11+
}
12+
const chunks = [];
13+
res.on('data', chunk => chunks.push(chunk));
14+
res.on('end', () => {
15+
try {
16+
if (res.statusCode === 204) cb(null, null);
17+
else cb(null, JSON.parse(Buffer.concat(chunks).toString()), res);
18+
} catch (e) {
19+
cb(e);
20+
}
21+
});
22+
});
23+
req.on('error', cb);
24+
req.end();
25+
}
26+
27+
function getDeployedHashes(headers, environments, cb) {
28+
const hashes = [];
29+
const envFound = new Array(environments ? environments.length : 2).fill(false);
30+
let pending = 1;
31+
function fetch(url) {
32+
apiCall(url, 'GET', headers, (err, responseJSON, response) => {
33+
if (err) {
34+
cb && cb(err);
35+
cb = null;
36+
return;
37+
}
38+
for (let deployment of responseJSON) {
39+
const index = environments ? environments.indexOf(deployment.environment) : 0;
40+
if (index >= 0 && deployment.statuses_url.startsWith('https://api.github.com/')) {
41+
++pending;
42+
apiCall(deployment.statuses_url, 'GET', headers, (err, statuses) => {
43+
if (err) {
44+
cb && cb(err);
45+
cb = null;
46+
return;
47+
}
48+
if (statuses[0] && statuses[0].state == 'success') {
49+
hashes.push(deployment.sha);
50+
envFound[index] = true;
51+
}
52+
if (!--pending && cb) {
53+
cb(null, hashes);
54+
cb = null;
55+
}
56+
});
57+
}
58+
}
59+
const match = (response.headers.link || '').match(/<(.[^>]+)>;\s*rel="next"/);
60+
if (cb && match && match[1] && match[1].startsWith('https://api.github.com/')) {
61+
if (!envFound.every(_ => _)) {
62+
fetch(match[1]);
63+
return;
64+
}
65+
}
66+
if (!--pending && cb) {
67+
cb(null, hashes);
68+
cb = null;
69+
}
70+
});
71+
}
72+
fetch('https://api.github.com/repos/rapidlua/luajit.me/deployments');
73+
}
74+
75+
function listDigitalOceanPrivateImages(headers, cb) {
76+
const images = [];
77+
function fetch(url) {
78+
apiCall(url, 'GET', headers, (err, response) => {
79+
if (err)
80+
return cb(err);
81+
images.push(...response.images);
82+
try {
83+
if (response.links.pages.next.startsWith('https://api.digitalocean.com/'))
84+
return fetch(response.links.pages.next);
85+
} catch (e) {}
86+
cb(null, images);
87+
});
88+
}
89+
fetch('https://api.digitalocean.com/v2/images?private=true&per_page=200');
90+
}
91+
92+
function isOrphanedImage(name, hashes) {
93+
if (!name.startsWith('image-'))
94+
return false;
95+
const res = child_process.spawnSync(
96+
'git', [ 'rev-parse', '--verify', 'v' + name.substr(6) ],
97+
{ encoding: 'utf8' }
98+
);
99+
return res.status || !hashes.includes(res.stdout.trim());
100+
}
101+
102+
function fatal(...args) {
103+
console.error(...args);
104+
process.exit(1);
105+
}
106+
107+
const githubHeaders = {
108+
'User-Agent': 'rapidlua',
109+
Authorization: 'token ' + process.env.GITHUB_TOKEN,
110+
Accept: 'application/vnd.github.ant-man-preview+json'
111+
};
112+
113+
const digitalOceanHeaders = {
114+
Authorization: 'Bearer ' + process.env.DIGITALOCEAN_TOKEN
115+
}
116+
117+
function hourToMSec(v) { return v * 1000 * 60 * 60; }
118+
119+
getDeployedHashes(githubHeaders, [ 'production' ], (err, hashes) => {
120+
if (err) fatal('get deployed hashes:', err);
121+
listDigitalOceanPrivateImages(digitalOceanHeaders, (err, images) => {
122+
if (err) fatal('list DigitalOcean private images:', err);
123+
for (let image of images) {
124+
if (image.type === 'snapshot' && Date.now() - new Date(image.created_at) > hourToMSec(3) && isOrphanedImage(image.name, hashes)) {
125+
apiCall('https://api.digitalocean.com/v2/images/' + image.id, 'DELETE', digitalOceanHeaders, (err) => {
126+
if (err) fatal('removing ' + image.name + ':', err);
127+
console.log('removed', image.name);
128+
});
129+
}
130+
}
131+
});
132+
});

.github/workflows/gc-cloud-images.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: GC Cloud Images
2+
3+
on:
4+
schedule:
5+
- cron: '* */3 * * *'
6+
7+
jobs:
8+
gc-cloud-images:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v1
12+
- run: >
13+
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN}} DIGITALOCEAN_TOKEN=${{ secrets.DIGITALOCEAN_TOKEN }} node .github/workflows/gc-cloud-images.js
14+

deploy/cloud-images.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"variables": {
3+
"version": "",
4+
"digitalocean_token": ""
5+
},
6+
"builders": [
7+
{
8+
"type": "digitalocean",
9+
"api_token": "{{user `digitalocean_token`}}",
10+
"image": "ubuntu-18-04-x64",
11+
"region": "ams3",
12+
"size": "s-1vcpu-1gb",
13+
"snapshot_name": "image-{{user `version`}}",
14+
"ssh_username": "root"
15+
}
16+
],
17+
"provisioners": [
18+
{
19+
"type": "shell",
20+
"inline": [
21+
"apt-get update",
22+
"DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends",
23+
24+
"apt-get install -y --no-install-recommends apt-transport-https curl",
25+
"apt-get install -y --no-install-recommends ca-certificates gnupg-agent software-properties-common",
26+
"curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -",
27+
"add-apt-repository \"deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\"",
28+
"apt-get update",
29+
"mkdir -p /etc/docker",
30+
"echo '{\"userns-remap\":\"default\"}' > /etc/docker/daemon.json",
31+
"apt-get install -y --no-install-recommends docker-ce docker-ce-cli containerd.io",
32+
"docker pull node:10-alpine",
33+
34+
"apt-get install -y --no-install-recommends nginx",
35+
"systemctl disable nginx"
36+
]
37+
}
38+
]
39+
}

0 commit comments

Comments
 (0)