blob: 24e4a994191706bc42542b640e8976709f64dd9f [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This file contains javascript which gets deployed to scripts.google.com, in
// order to power custom logic for the internal RFC tracker.
const TITLE_COL = 'Title';
const CL_COL = 'CL';
const CHANGE_ID_COL = 'Change Id';
const AUTHORS_COL = 'Author(s)';
const SUBMITTED_COL = 'Submitted';
const STATUS_COL = 'Status';
const NEEDS_SYNC_COL = 'Needs Sync';
const CL_UPDATED_COL = 'CL Updated';
const WIP_STATUS = 'Socialization';
const DEFAULT_STATUS = 'Draft';
const ABANDONED_STATUS = 'Withdrawn';
const RFCS_DIR = 'docs/contribute/governance/rfcs';
const SUBJECT_TAG = '[rfc]';
// Run _fetchRfcs() on the main 'prod' tracker.
function fetchRfcs() {
_fetchRfcs('bbfvcDiuyZ08S5ZJ7vN4ZI');
}
// Run _fetchRfcs() on a test copy of the tracker.
function fetchRfcsTest() {
// Put your personal test Table's ID here:
_fetchRfcs('');
}
// Gets the list of open RFC CLs from gerrit and compares that to the set of
// RFCs in the tracker. For any CLs missing from the tracker, creates stub rows
// with just the change_id filled in.
function _fetchRfcs(tableId) {
const gerritCls = _fetchOpenRfcsCls();
const trackerCls = _getExistingRfcs(tableId);
// Accumulate create requests so that requests can be batched.
let createRows = [];
for (const changeId of gerritCls) {
if (!(changeId in trackerCls)) {
createRows.push({
row: {
values: {
[CHANGE_ID_COL]: changeId,
}
}
});
}
}
if (createRows.length != 0) {
Area120Tables.Tables.Rows.batchCreate({ requests: createRows }, `tables/${tableId}`);
}
}
// Returns an Array of change_ids.
function _fetchOpenRfcsCls() {
const data = parseGerritResponse(UrlFetchApp.fetch(
GERRIT_API_URL + '/changes/?q='
+ 'dir:' + encodeURIComponent(RFCS_DIR)
+ '+is:open'
+ '&n=100'));
const changeIds = [];
for (const cl of data) {
if (cl.subject.toLowerCase().startsWith(SUBJECT_TAG)) {
changeIds.push(cl.change_id);
}
}
return changeIds;
}
// Returns {[change_id]: {name: 'row_name'}, ...}
function _getExistingRfcs(tableId) {
// In order to limit total response sizes,
// https://developers.google.com/apps-script/advanced/tables?hl=en#read_rows_of_a_table has clients
// read through the rows in units of pages, which we do here so that this script won't fail when
// there are more than N RFCs
const PAGE_SIZE = 1000;
let response = Area120Tables.Tables.Rows.list(`tables/${tableId}`, { page_size: PAGE_SIZE });
let rfcs = {};
while (response && 'rows' in response) {
let rows = response.rows;
// Add existing RFCs to the object
for (let i = 0; i < rows.length; i++) {
let change_id = rows[i].values[CHANGE_ID_COL];
if (change_id == null) {
continue;
}
rfcs[change_id] = { name: rows[i].name };
}
// Read next page of rows
let pageToken = response.nextPageToken;
if (!pageToken) {
response = undefined;
} else {
response = Area120Tables.Tables.Rows.list(`tables/${tableId}`, { page_size: PAGE_SIZE, page_token: pageToken });
}
}
return rfcs;
}
// Apply any programmatic updates we might want to make to the given row.
// Specifically, fetch the latest state from gerrit and update the row to
// match.
function syncRow(tableId, rowId) {
const rowName = `tables/${tableId}/rows/${rowId}`;
const row = Area120Tables.Tables.Rows.get(rowName);
console.log('Row before: ', row);
const changeId = row.values[CHANGE_ID_COL];
const cl = parseGerritResponse(UrlFetchApp.fetch(
// include _account_id, email and username fields when referencing accounts.
GERRIT_API_URL + `/changes/${changeId}?o=DETAILED_ACCOUNTS`
));
// Remove subject prefix tags (e.g. '[rfc][docs]')
const title = cl.subject.replace(/^(\S+]\s)/i, '');
const patchedValues = {
[TITLE_COL]: title,
[CL_COL]: `fxrev.dev/${cl._number}`,
[AUTHORS_COL]: cl.owner.email,
[SUBMITTED_COL]: cl.created,
[CL_UPDATED_COL]: cl.updated,
[NEEDS_SYNC_COL]: false,
};
if (cl.work_in_progress) {
if (!row.values[STATUS_COL]) {
patchedValues[STATUS_COL] = WIP_STATUS;
}
} else {
if (!row.values[STATUS_COL] || row.values[STATUS_COL] === WIP_STATUS) {
patchedValues[STATUS_COL] = DEFAULT_STATUS;
}
}
// Abandoning the CL amounts to withdrawing the RFC.
if (cl.status === 'ABANDONED') {
patchedValues[STATUS_COL] = ABANDONED_STATUS;
}
Area120Tables.Tables.Rows.patch({ values: patchedValues }, rowName);
}
// Returns {[change_id]: true, ...}
function _getExistingRfcsFromAppSheet() {
const rows = callAppSheetAPI({
"Action": "Find",
"Properties": {},
});
const res = {};
for (const row of rows) {
res[row['Change ID']] = true;
}
return res;
}
// Gets the list of open RFC CLs from gerrit and compares that to the set of
// RFCs in the AppSheet tracker. For any CLs missing from the tracker, creates
// stub rows with just the change_id filled in.
function addNewRfcsToAppSheet() {
const gerritCls = _fetchOpenRfcsCls();
const trackerCls = _getExistingRfcsFromAppSheet();
// Accumulate create requests so that requests can be batched.
let createRows = [];
for (const changeId of gerritCls) {
if (!(changeId in trackerCls)) {
createRows.push({ [CHANGE_ID_COL]: changeId });
}
}
if (createRows.length != 0) {
callAppSheetAPI({
"Action": "Add",
"Properties": {},
Rows: createRows,
});
}
}
// Call the gerrit API and return some specific information about the named CL.
function getClInfo(changeId) {
const cl = parseGerritResponse(UrlFetchApp.fetch(
// include _account_id, email and username fields when referencing accounts.
GERRIT_API_URL + `/changes/${changeId}?o=DETAILED_ACCOUNTS`
));
// Remove subject prefix tags (e.g. '[rfc][docs]')
const title = cl.subject.replace(/^(\S+]\s)/i, '');
return {
title,
number: cl._number,
author: cl.owner.email,
created: cl.created,
updated: cl.updated,
status: cl.status,
work_in_progress: Boolean(cl.work_in_progress),
};
}