Hasta ahora hay varios approaches que he intentado para solucionar el problema actual de Hashbot, el problema parte luego de crear el repositorio, cuando intentamos hacer requests enseguida luego de crearlo, la api devuelve dos errores distintos.
Utilizando octonode, puedo utilizar el método createContents
para crear los archivos commit tras commit por separado.
function addFilestoRepository(data) {
var repository = format("{company}/{repository}", { company: config.COMPANY_GITHUB_USER, repository: data.name });
ghrepoPromise = Promise.promisifyAll(client.repo(repository));
var teamGithubAccounts = team.githubUsers.join('\n');
var files = [
{ filename: 'README.md', url: 'lib/assetFiles/README.md', vars: { repositoryName: data.name } },
{ filename: 'CHANGELOG.md', url: 'lib/assetFiles/CHANGELOG.md', vars: { day: dateformat(new Date(), "yyyy-mm-dd") }},
{ filename: 'LICENSE.md', url: 'lib/assetFiles/LICENSE.md' },
{ filename: '.github/ISSUE_TEMPLATE.md', url: 'lib/assetFiles/ISSUE_TEMPLATE.md' },
{ filename: '.github/PULL_REQUEST_TEMPLATE.md', url: 'lib/assetFiles/PULL_REQUEST_TEMPLATE.md' },
{ filename: '.mention-bot', url: 'lib/assetFiles/mention-bot' },
{ filename: 'MAINTAINERS', url: 'lib/assetFiles/MAINTAINERS', vars: { maintainers: teamGithubAccounts }},
{ filename: '.lgtm', url: 'lib/assetFiles/lgtm' }
];
return [data, ghrepoPromise, Promise.map(files, function(file) {
var commitMsg = format('adding {file}', { file: file.filename });
return fs.readFileAsync(file.url, "utf-8").then(function(contents) {
var templatedContents = contents;
if(file.hasOwnProperty('vars')) {
templatedContents = format(contents, file.vars);
}
return ghrepoPromise.createContentsAsync(file.filename, commitMsg, templatedContents);
});
})];
}
Utilizando Promise.each los archivos se van creando uno por uno, luego de que el anterior ha sido creado (no ocurre en paralelo).
Sin embargo, octonode devuelve un error algunas veces al intentar crear los archivos:
StatusCodeError: 409 - {"message":"refs/heads/master is at 937e0a4ca339cc55f6a0a8fb4019a88660b45240 but expected 8f366bf7dc6f0d66b36d248298278781e711f7d3","documentation_url":"https://developer.github.com/v3/repos/contents/"}
Otro approach que intenté fue crearlos manualmente, y asi verificar si el problema es en octonode o no:
function addFilestoRepository(data) {
var repository = format("{company}/{repository}", { company: config.COMPANY_GITHUB_USER, repository: data.name });
ghrepoPromise = Promise.promisifyAll(client.repo(repository));
var teamGithubAccounts = team.githubUsers.join('\n');
var files = [
{ filename: 'README.md', url: 'lib/assetFiles/README.md', vars: { repositoryName: data.name } },
{ filename: 'CHANGELOG.md', url: 'lib/assetFiles/CHANGELOG.md', vars: { day: dateformat(new Date(), "yyyy-mm-dd") }},
{ filename: 'LICENSE.md', url: 'lib/assetFiles/LICENSE.md' },
{ filename: '.github/ISSUE_TEMPLATE.md', url: 'lib/assetFiles/ISSUE_TEMPLATE.md' },
{ filename: '.github/PULL_REQUEST_TEMPLATE.md', url: 'lib/assetFiles/PULL_REQUEST_TEMPLATE.md' },
{ filename: '.mention-bot', url: 'lib/assetFiles/mention-bot' },
{ filename: 'MAINTAINERS', url: 'lib/assetFiles/MAINTAINERS', vars: { maintainers: teamGithubAccounts }},
{ filename: '.lgtm', url: 'lib/assetFiles/lgtm' }
];
return [data, ghrepoPromise, Promise.each(files, function(file) {
var commitMsg = format('adding {file}', { file: file.filename });
return fs.readFileAsync(file.url, "utf-8").then(function(contents) {
var templatedContents = contents;
if(file.hasOwnProperty('vars')) {
templatedContents = format(contents, file.vars);
}
var base64Content = new Buffer(templatedContents).toString('base64');
var options = {
headers: {
"Authorization": format("token {token}", { token: config.GITHUB_KEY }),
"User-Agent": "Hashbot"
},
json: true,
method: "PUT",
uri: format("https://api.github.com/repos/{repoName}/contents/{filepath}",
{ repoName: data.full_name, filepath: file.filename }),
body: {
content: base64Content,
message: commitMsg,
path: file.filename
}
};
return request(options);
});
})];
}
Acá estoy creando las requests a través de la libreria request-promise
. Sin embargo, al hacer esta request la api devuelve un mensaje de error 404, que ocurre apenas al iniciar la request con el primer archivo:
StatusCodeError: 404 - {"message":"Not Found","documentation_url":"https://developer.github.com/v3"
También intenté crear los archivos en un solo commit, con este approach, el problema del error 409 donde las referencias vuelven mal desaparece, pero persiste el error 404 de la api.
El approach está definido en este articulo: http://mdswanson.com/blog/2011/07/23/digging-around-the-github-api-take-2.html
En la implementación, separé el código para obtener el contenido de los archivos, del código para hacer las requests solicitando las refs y los sha necesarios:
function obtainFileContents(data) {
var repository = format("{company}/{repository}", { company: config.COMPANY_GITHUB_USER, repository: data.name });
ghrepoPromise = Promise.promisifyAll(client.repo(repository));
var teamGithubAccounts = team.githubUsers.join('\n');
var files = [
{ filename: 'README.md', url: 'lib/assetFiles/README.md', vars: { repositoryName: data.name } },
{ filename: 'CHANGELOG.md', url: 'lib/assetFiles/CHANGELOG.md', vars: { day: dateformat(new Date(), "yyyy-mm-dd") }},
{ filename: 'LICENSE.md', url: 'lib/assetFiles/LICENSE.md' },
{ filename: '.github/ISSUE_TEMPLATE.md', url: 'lib/assetFiles/ISSUE_TEMPLATE.md' },
{ filename: '.github/PULL_REQUEST_TEMPLATE.md', url: 'lib/assetFiles/PULL_REQUEST_TEMPLATE.md' },
{ filename: '.mention-bot', url: 'lib/assetFiles/mention-bot' },
{ filename: 'MAINTAINERS', url: 'lib/assetFiles/MAINTAINERS', vars: { maintainers: teamGithubAccounts }},
{ filename: '.lgtm', url: 'lib/assetFiles/lgtm' }
];
return [data, ghrepoPromise, Promise.all(files.map(function (file) {
return fs.readFileAsync(file.url, "utf-8").then(function (contents) {
var templatedContents = contents;
if (file.hasOwnProperty('vars')) {
templatedContents = format(contents, file.vars);
}
return {
file,
templatedContents
};
});
}))];
}
function createInitialCommitWithContents(repoData, ghrepoPromise, files) {
var defaultOptions = {
headers: {
"Authorization": format("token {token}", { token: config.GITHUB_KEY }),
"User-Agent": "Hashbot"
},
json: true
};
// getting the SHA for the latest commit
var shaCommitOptions = _.extend({}, defaultOptions, {
method: "GET",
uri: format("https://api.github.com/repos/{repoName}/git/refs/heads/master", { repoName: repoData.full_name })
});
return request(shaCommitOptions)
.then(function (result) {
return result.object.sha;
})
.then(function (shaLatestCommit) {
// getting the SHA for the latest tree
var shaBaseTreeOptions = _.extend({}, defaultOptions, {
method: "GET",
uri: format("https://api.github.com/repos/{repoName}/git/commits/{sha}", { repoName: repoData.full_name, sha: shaLatestCommit })
});
return [shaLatestCommit, request(shaBaseTreeOptions)
.then(function (result) {
return result.tree.sha;
})];
})
.spread(function (shaLatestCommit, shaBaseTree) {
// creating the tree
var createTreeOptions = _.extend({}, defaultOptions, {
method: "POST",
uri: format("https://api.github.com/repos/{repoName}/git/trees", { repoName: repoData.full_name }),
body: {
tree: files.map(function (file) {
return {
path: file.file.filename,
mode: "100644",
type: "blob",
content: file.templatedContents
};
})
}
});
return [shaLatestCommit, request(createTreeOptions)
.then(function (result) {
return result.sha;
})];
})
.spread(function (shaLatestCommit, shaLatestTree) {
// creating the commit
var createCommitOptions = _.extend({}, defaultOptions, {
method: "POST",
uri: format("https://api.github.com/repos/{repoName}/git/commits", { repoName: repoData.full_name }),
body: {
"message": "Initial project structure",
"tree": shaLatestTree,
"parents": [shaLatestCommit]
}
});
return request(createCommitOptions)
.then(function (result) {
return result.sha;
});
})
.then(function (shaNewCommit) {
// updating latest reference
var updateRefOptions = _.extend({}, defaultOptions, {
method: "PATCH",
uri: format("https://api.github.com/repos/{repoName}/git/refs/heads/master", { repoName: repoData.full_name }),
body: {
"sha": shaNewCommit,
"force": true
}
});
return [repoData, ghrepoPromise, request(updateRefOptions)];
});
}
Hay dos problemas distintos en esto, cuando creamos el repositorio y ejecutamos requests sucesivas muy rápido en él, nos da el error 404. Y cuando creamos los archivos por separado, y los creamos sucesivamente muy rápido, tenemos el error 409.
Utilizando el último approach para crearlos con un solo commit, y añadiendo un delay con bluebird: Promise.delay(5000)
los errores desaparecen.
Abstraje el código a su propio file: https://github.com/hashlabshq/hashbot/blob/cm/%2353/lib/github_api_helper.js
Lo que dejaría el metodo en github_integration.js de esta forma: https://github.com/hashlabshq/hashbot/blob/cm/%2353/lib/github_integration.js#L49-L57
Tomando en cuenta que estoy seteando este delay: https://github.com/hashlabshq/hashbot/blob/cm/%2353/lib/github_integration.js#L125