Merge branch 'master' of https://github.com/keymanapp/keyman into flick-longpress

This commit is contained in:
Meng-Heng 2025-06-24 16:47:26 +07:00
commit d0add3fa4e
80 changed files with 13727 additions and 214 deletions

183
.github/workflows/pr-build-status.yml vendored Normal file
View file

@ -0,0 +1,183 @@
name: Keyman Build Summary
on:
# Temporary for testing:
push:
branches:
- "maint/resources/14172-pr-build-status-2"
check_run:
types: [completed]
jobs:
run_pr_build_status:
name: Summarize build status checks
runs-on: ubuntu-latest
steps:
- name: Check PR build status
id: run_pr_build_status_script
uses: actions/github-script@v7
with:
script: |
// This code is copied out of resources/build/pr-build-status/pr-build-status.mjs
// where it is tested. It is copied inline here in order to avoid requiring the
// repository to be checked out, which dramatically reduces the run time of the
// check.
//
// Note: we don't currently look at check runs, only statuses
//
// Verify the following statuses:
// 'user_testing'
// 'API Verification' (github-actions[bot])
//
// At least 1 of the following statuses must be found:
// 'Test*' (keyman-server), e.g. 'Test Build (Keyman)'
// 'Ubuntu Packaging' (github-actions[bot])
//
// Ignore the following statuses:
// check/web/file-size
//
function reduceStatuses(statuses) {
const filtered_statuses = statuses.reduce((o, status) => {
if(status.creator?.login == 'keyman-server' && status.context.startsWith('Test')) {
if(!o[status.context]) o[status.context] = {type: 'build', state: status.state};
} else if(status.creator?.login == 'keymanapp-test-bot[bot]' && status.context == 'user_testing') {
if(!o[status.context]) o[status.context] = {type: 'user-test', state: status.state};;
} else if(status.context == 'API Verification') {
if(!o[status.context]) o[status.context] = {type: 'check', state: status.state};
} else if(status.context == 'Ubuntu Packaging') {
if(!o[status.context]) o[status.context] = {type: 'build', state: status.state};
} else if(status.context == 'check/web/file-size') {
// Ignore check/web/file-size -- we won't block automerge for this at this point
} else {
// We fail with an 'unknown status' response if we get a new status check
// so we can be sure we are not skipping known status checks
o[status.context] = {type: 'unknown', state: status.state};
}
return o;
}, {});
return filtered_statuses;
}
//
// Given the collection of status checks we care about, return
// an aggregate status -- error, failed, pending, or success,
// and a summary description
//
function calculateFinalStatus(filtered_statuses) {
const counts = {};
let hasBuilds = false;
for(const context of Object.keys(filtered_statuses)) {
const { state, type } = filtered_statuses[context];
if(type == 'unknown') {
// We special-case for unknown status checks, and never permit them
return [
'error', `An unknown context ${context} was found, cannot calculate build status.`
];
}
if(type == 'build') {
hasBuilds = true;
}
counts[state] = counts[state] ? counts[state] + 1 : 1;
}
// If we do not have any statuses yet, we wait
if(Object.keys(filtered_statuses).length == 0 || !hasBuilds) {
return ['pending', 'Checks have not yet been triggered ⌛'];
}
const state =
counts.error ? 'error' :
counts.failed ? 'failed' :
counts.pending ? 'pending' :
'success';
let description = '';
function appendDescription(count, state) {
if(!count) return;
if(description != '') description += '; ';
description += `${count} check${count == 1 ? '' : 's'} ${state}`;
}
appendDescription(counts.error, 'in an error state ❌');
appendDescription(counts.failed, 'failed ❌');
appendDescription(counts.pending, 'pending ⌛');
appendDescription(counts.success, 'completed successfully ✅');
return [ state, description ];
}
async function getCommitStatuses(github, owner, repo, sha) {
const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{sha}/statuses', {
owner,
repo,
sha,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
return statuses;
}
async function getCommitCheckRuns(github, owner, repo, sha) {
const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{sha}/check-runs', {
owner,
repo,
sha,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
return statuses;
}
function calculateCheckResult(statuses) {
if(!Array.isArray(statuses)) {
return ['error', 'Failed to retrieve status checks from GitHub ❌'];
}
const filtered_statuses = reduceStatuses(statuses);
const result = calculateFinalStatus(filtered_statuses);
return result;
}
async function test(github, owner, repo, sha) {
// Get statuses from sha
const statuses = await getCommitStatuses(github, owner, repo, sha);
return calculateCheckResult(statuses);
}
async function createCheck(github, owner, repo, sha) {
const check = await github.rest.checks.create({
owner,
repo,
head_sha: sha,
name: 'Build Outcome',
status: 'in_progress',
});
return check.data.id;
}
async function updateCheck(github, owner, repo, checkRunId, status, description) {
const checkStatus = status == 'pending' ? 'in_progress' : 'completed';
const conclusion = checkStatus == 'in_progress' ? undefined : (status == 'success' ? 'success' : 'failure');
await github.rest.checks.update({
owner,
repo,
check_run_id: checkRunId,
status: checkStatus,
conclusion,
output: {
title: description,
summary: ''
}
});
}
const { owner, repo } = context.repo;
const sha = context.payload?.check_suite?.sha || context.sha;
const checkRunId = await createCheck(github, owner, repo, sha);
const res = await test(github, owner, repo, sha);
await updateCheck(github, owner, repo, checkRunId, res[0], res[1]);

View file

@ -1,5 +1,9 @@
# Keyman Version History
## 19.0.67 alpha 2025-06-22
* chore(developer): ldml: additional line numbers (#14104)
## 19.0.66 alpha 2025-06-16
* fix(windows): add values wucUpdateAvailable and wucNotChecked to TRemoteUpdateCheckResult enum (#14123)

View file

@ -1 +1 @@
19.0.67
19.0.68

View file

@ -393,7 +393,7 @@ export class UnicodeSetItem extends VarsItem {
super(id, value, sections, x);
const needRanges = sections.usetparser.sizeUnicodeSet(value);
if (needRanges >= 0) {
this.unicodeSet = sections.usetparser.parseUnicodeSet(value, needRanges);
this.unicodeSet = sections.usetparser.parseUnicodeSet(value, needRanges, x);
} // otherwise: error (was recorded via callback)
}
unicodeSet?: UnicodeSet;

View file

@ -392,4 +392,4 @@ export class NFDAnalyzer extends StringAnalyzer {
return null;
}
}
}
}

View file

@ -1,8 +1,9 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m } from './compiler-interfaces.js';
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m, CompilerMessageObjectSpec as mx } from './compiler-interfaces.js';
import { constants } from '@keymanapp/ldml-keyboard-constants';
import { ObjectWithMetadata } from './symbol-utils.js';
const DeveloperUtilsErrMask = CompilerErrorNamespace.DeveloperUtils;
// const SevInfo = CompilerErrorSeverity.Info | DeveloperUtilsErrMask;
@ -14,8 +15,10 @@ const SevError = CompilerErrorSeverity.Error | DeveloperUtilsErrMask;
export class DeveloperUtilsMessages {
// structured Ajv validation error
static ERROR_SchemaValidationError = SevError | 0x0001;
static Error_SchemaValidationError = (o:{instancePath:string, keyword:string, message: string, params: string}) => m(this.ERROR_SchemaValidationError,
`Error validating LDML XML file: ${def(o.instancePath)}: ${def(o.keyword)}: ${def(o.message)} ${def(o.params)}`);
static Error_SchemaValidationError = (o:{instancePath:string, keyword:string, message: string, params: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_SchemaValidationError, x,
`Error validating LDML XML file: ${def(o.instancePath)}: ${def(o.keyword)}: ${def(o.message)} ${def(o.params)}`,
);
static ERROR_ImportInvalidBase = SevError | 0x0002;
static Error_ImportInvalidBase = (o: { base: string, path: string, subtag: string }) =>

View file

@ -67,7 +67,7 @@ export { UrlSubpathCompilerCallback } from './utils/UrlSubpathCompilerCallback.j
export { DeveloperUtilsMessages } from './developer-utils-messages.js';
export * as SourceFilenamePatterns from './source-filename-patterns.js';
export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader, KeymanXMLMetadata, XML_FILENAME_SYMBOL } from './xml-utils.js';
export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader, KeymanXMLMetadata, XML_FILENAME_SYMBOL, withOffset } from './xml-utils.js';
export { SymbolUtils, ObjectWithMetadata } from './symbol-utils.js';
export * as LineUtils from './line-utils.js';
export * as GitHubUrls from './github-urls.js';

View file

@ -12,7 +12,7 @@ import { constants } from '@keymanapp/ldml-keyboard-constants';
import { LDMLKeyboardTestDataXMLSourceFile, LKTTest, LKTTests } from './ldml-keyboard-testdata-xml.js';
import boxXmlArray = util.boxXmlArray;
import { LineFinderEventResolver } from '../../line-utils.js';
import { XML_FILENAME_SYMBOL, KeymanXMLReader } from '../../xml-utils.js';
import { XML_FILENAME_SYMBOL, KeymanXMLReader, findInstanceObject } from '../../xml-utils.js';
interface NameAndProps {
'$'?: any; // content
@ -310,12 +310,13 @@ export class LDMLKeyboardXMLSourceFileReader implements EventResolver {
public validate(source: LDMLKeyboardXMLSourceFile | LDMLKeyboardTestDataXMLSourceFile): boolean {
if(!SchemaValidators.default.ldmlKeyboard3(source)) {
for (const err of (<any>SchemaValidators.default.ldmlKeyboard3).errors) {
const context = findInstanceObject(source, err?.instancePath?.split('/'));
this.callbacks.reportMessage(DeveloperUtilsMessages.Error_SchemaValidationError({
instancePath: err.instancePath,
keyword: err.keyword,
message: err.message || 'Unknown AJV Error', // docs say 'message' is optional if 'messages:false' in options
params: Object.entries(err.params || {}).sort().map(([k,v])=>`${k}="${v}"`).join(' '),
}));
}, context));
}
return false;
}

View file

@ -360,3 +360,40 @@ export class KeymanXMLWriter {
}
}
/**
* traverse an AJV instancePath and map to an object if possible
* @param source object tree root (contains the root object)
* @param path ajv split instancePath, such as '/keyboard3/layers/0'.split('/')
* @returns undefined if the path was not present, null if path went to something that wasn't an object, otherwise the compileContext object is returned.
*/
export function findInstanceObject(source: any, path: string[]) : any {
if(!path || !source || path.length == 0) {
return source;
} else if(path[0] == '') {
return findInstanceObject(source, path.slice(1));
} else if(Array.isArray(source) || typeof source == 'object') {
const child = source[path[0]];
if (child == undefined) return child; // nothing here
if (!child || typeof child == 'string') {
return source; // return the *parent* object if the child is empty (could be a property)
}
return findInstanceObject(child, path.slice(1));
} else {
return null;
}
}
/**
* Return an object simulating an XML object with an offset number
* For use in calling message functions
* @param c number for the offset setting
* @param x if set, this object will be used as the base object instead of {}
*/
export function withOffset(c: number, x?: any) : KeymanXMLMetadata {
// set metadata on an empty object
const o = Object.assign({}, x);
KeymanXMLReader.setMetaData(o, {
startIndex: c
});
return o;
}

View file

@ -8,7 +8,7 @@ import { testReaderCases } from '../helpers/reader-callback-test.js';
import CLDRScanToVkey = Constants.CLDRScanToVkey;
import CLDRScanToKeyMap = Constants.CLDRScanToKeyMap;
import USVirtualKeyCodes = Constants.USVirtualKeyCodes;
import { KeymanXMLReader, XML_FILENAME_SYMBOL } from '../../src/xml-utils.js';
import { KeymanXMLReader, withOffset, XML_FILENAME_SYMBOL } from '../../src/xml-utils.js';
function pluckKeysFromKeybag(keys: LKKey[], ids: string[]) {
return keys.filter(({id}) => ids.indexOf(id) !== -1);
@ -25,7 +25,7 @@ describe('ldml keyboard xml reader tests', function () {
keyword: 'required',
message: `must have required property 'info'`,
params: 'missingProperty="info"',
})],
}, withOffset(39))],
},
{
subpath: 'invalid-conforms-to.xml',
@ -34,7 +34,7 @@ describe('ldml keyboard xml reader tests', function () {
keyword: 'enum',
message: `must be equal to one of the allowed values`,
params: 'allowedValues="45,46"', // this has to be kept in sync with the DTD
})],
}, withOffset(39))],
},
{
subpath: 'import-minimal.xml',

View file

@ -12,7 +12,7 @@ import { env } from 'node:process';
import { readFileSync, writeFileSync } from 'node:fs';
import { KeymanXMLType, KeymanXMLReader, KeymanXMLWriter } from '../src/xml-utils.js';
import { KeymanXMLType, KeymanXMLReader, KeymanXMLWriter, findInstanceObject } from '../src/xml-utils.js';
import { LineFinder } from '../src/line-utils.js';
import { makePathToFixture } from './helpers/index.js';
import { SymbolUtils } from '../src/symbol-utils.js';
@ -198,5 +198,36 @@ describe(`XML Reader line number test`, () => {
LineFinder.offsetToLineColumn(
getMetaData(actual.keyboard3.transforms).startIndex, lines), { line: 8, column: 2 });
});
describe('findInstanceObject test', () => {
const path0 = '/keyboard3/layers/0';
const TARGET = Symbol("Looking for this!");
it(`Should be able to parse ${path0}`, () => {
const o = {
keyboard3: {
layers: [
TARGET,
]
}
};
assert.equal(findInstanceObject(o, path0.split('/')), TARGET);
});
// path to property
const path1 = '/keyboard3/conformsTo';
it(`Should be able to parse ${path1}`, () => {
const keyboard3 = { conformsTo: "1234"};
const o = {
keyboard3,
};
assert.equal(findInstanceObject(o, path1.split('/')), keyboard3);
});
const path2 = '/keyboard3/bad/path';
it(`Should be able to handle ${path2}`, () => {
const keyboard3 = { conformsTo: "1234"};
const o = {
keyboard3,
};
assert.equal(findInstanceObject(o, path2.split('/')), undefined);
});
});
});

View file

@ -51,7 +51,7 @@ function do_clean() {
#-------------------------------------------------------------------------------------------------------------------
function do_publish() {
verify-program-signatures
builder_if_release_build_level verify-program-signatures
verify-node-installer-version
"$KEYMAN_ROOT/common/windows/cef-checkout.sh"
@ -80,7 +80,7 @@ function do_publish() {
copy-kmdev
verify-installer-signatures
builder_if_release_build_level verify-installer-signatures
}
function do_test() {

View file

@ -32,7 +32,7 @@ function do_build() {
build_version.res
vs_msbuild kmanalyze.vcxproj //t:Build "//p:Platform=Win32"
cp "$WIN32_TARGET" "$DEVELOPER_PROGRAM"
cp "$WIN32_TARGET_PATH/kmanalyze.pdb" "$DEVELOPER_DEBUGPATH"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmanalyze.pdb" "$DEVELOPER_DEBUGPATH"
}
function do_publish() {

View file

@ -60,7 +60,7 @@ export class LayrCompiler extends SectionCompiler {
if (totalLayerCount === 0) { // TODO-LDML: does not validate touch layers yet
// no layers seen anywhere
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(this.keyboard3?.layers[0]));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(this.keyboard3));
}
return valid;
}

View file

@ -97,7 +97,7 @@ export class VarsCompiler extends SectionCompiler {
// Sets
for (const e of variables.set) {
const { id, value } = e;
if(!this.validateIdentifier(id)) {
if(!this.validateIdentifier(id, e)) {
valid = false;
continue;
}
@ -124,7 +124,7 @@ export class VarsCompiler extends SectionCompiler {
// UnicodeSets
for (const e of variables.uset) {
const { id, value } = e;
if(!this.validateIdentifier(id)) {
if(!this.validateIdentifier(id, e)) {
valid = false;
continue;
}
@ -291,7 +291,7 @@ export class VarsCompiler extends SectionCompiler {
let { value } = e;
// fix any variables
value = result.substituteStrings(value, sections);
result.strings.push(new StringVarItem(id, value, sections));
result.strings.push(new StringVarItem(id, value, sections, e));
}
addSet(result: Vars, e: LDMLKeyboard.LKSet, sections: DependencySections): void {
const { id } = e;
@ -306,14 +306,14 @@ export class VarsCompiler extends SectionCompiler {
// this is not 'forMatch', all variables are to be assumed as string literals, not regex
// content.
const cookedItems: string[] = rawItems.map(v => result.substituteMarkerString(v, false));
result.sets.push(new SetVarItem(id, cookedItems, sections));
result.sets.push(new SetVarItem(id, cookedItems, sections, e));
}
addUnicodeSet(result: Vars, e: LDMLKeyboard.LKUSet, sections: DependencySections): void {
const { id } = e;
let { value } = e;
value = result.substituteStrings(value, sections);
value = result.substituteUnicodeSets(value, sections);
result.usets.push(new UnicodeSetItem(id, value, sections, sections.usetparser));
result.usets.push(new UnicodeSetItem(id, value, sections, sections.usetparser, e));
}
// routines for using/substituting variables have been moved to the Vars class and its
// properties

View file

@ -1,11 +1,12 @@
import 'mocha';
import {assert} from 'chai';
import { DispCompiler } from '../src/compiler/disp.js';
import { compilerTestCallbacks, loadSectionFixture, testCompilationCases, withOffset } from './helpers/index.js';
import { compilerTestCallbacks, loadSectionFixture, testCompilationCases } from './helpers/index.js';
import { KMXPlus } from '@keymanapp/common-types';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import Disp = KMXPlus.Disp;
import { withOffset } from '@keymanapp/developer-utils';
describe('disp', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while

View file

@ -8,7 +8,7 @@
<key id="one" output="1" />
</keys>
<layers formId="touch">
<layers formId="touch" minDeviceWidth="123">
<!-- INVALID: need at least one layer. -->
</layers>
</keyboard3>

View file

@ -8,7 +8,7 @@ import * as path from 'path';
import { fileURLToPath } from 'url';
import { SectionCompiler, SectionCompilerNew } from '../../src/compiler/section-compiler.js';
import { util, KMXPlus, LdmlKeyboardTypes } from '@keymanapp/common-types';
import { CompilerEvent, compilerEventFormat, CompilerCallbacks, LDMLKeyboardXMLSourceFileReader, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboard, KeymanXMLMetadata, KeymanXMLReader, CompilerError } from "@keymanapp/developer-utils";
import { CompilerEvent, compilerEventFormat, CompilerCallbacks, LDMLKeyboardXMLSourceFileReader, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboard, CompilerError } from "@keymanapp/developer-utils";
import { LdmlKeyboardCompiler } from '../../src/main.js'; // make sure main.js compiles
import { assert } from 'chai';
import { KMXPlusMetadataCompiler } from '../../src/compiler/metadata-compiler.js';
@ -358,17 +358,3 @@ export function hex_str(s?: string) : string {
return [...s].map(ch => dontEscape.test(ch) ? ch : util.escapeRegexChar(ch)).join('');
}
/**
* Return an object simulating an XML object with an offset number
* For use in calling message functions
* @param c number for the offset setting
* @param x if set, this object will be used as the base object instead of {}
*/
export function withOffset(c: number, x?: any) : KeymanXMLMetadata {
// set metadata on an empty object
const o = Object.assign({}, x);
KeymanXMLReader.setMetaData(o, {
startIndex: c
});
return o;
}

View file

@ -1,7 +1,7 @@
import 'mocha';
import { assert } from 'chai';
import { KeysCompiler } from '../src/compiler/keys.js';
import { assertCodePoints, compilerTestCallbacks, loadSectionFixture, testCompilationCases, withOffset } from './helpers/index.js';
import { assertCodePoints, compilerTestCallbacks, loadSectionFixture, testCompilationCases } from './helpers/index.js';
import { KMXPlus, Constants, LdmlKeyboardTypes } from '@keymanapp/common-types';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import { constants } from '@keymanapp/ldml-keyboard-constants';
@ -9,7 +9,7 @@ import { MetaCompiler } from '../src/compiler/meta.js';
const keysDependencies = [ ...BASIC_DEPENDENCIES, MetaCompiler ];
import Keys = KMXPlus.Keys;
import { BASIC_DEPENDENCIES } from '../src/compiler/empty-compiler.js';
import { LDMLKeyboard } from '@keymanapp/developer-utils';
import { LDMLKeyboard, withOffset } from '@keymanapp/developer-utils';
const K = Constants.USVirtualKeyCodes;
describe('keys', function () {

View file

@ -2,12 +2,13 @@ import 'mocha';
import { assert } from 'chai';
import { LayrCompiler } from '../src/compiler/layr.js';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import { compilerTestCallbacks, testCompilationCases, withOffset } from './helpers/index.js';
import { compilerTestCallbacks, testCompilationCases } from './helpers/index.js';
import { KMXPlus } from '@keymanapp/common-types';
import { constants } from '@keymanapp/ldml-keyboard-constants';
import Layr = KMXPlus.Layr;
import LayrRow = KMXPlus.LayrRow;
import { withOffset } from '@keymanapp/developer-utils';
function allKeysOk(row : LayrRow, str : string, msg? : string) {
const split = str.split(' ');
@ -107,13 +108,13 @@ describe('layr', function () {
{
// missing layer element
subpath: 'sections/layr/invalid-missing-layer.xml',
errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(withOffset(258))],
errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(withOffset(40))],
retainOffsetInMessages: true,
},
{
// missing layer element
subpath: 'sections/layr/invalid-missing-layer2.xml',
errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement()],
errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(withOffset(40))],
retainOffsetInMessages: true,
},
{

View file

@ -1,11 +1,12 @@
import 'mocha';
import { assert } from 'chai';
import { LocaCompiler } from '../src/compiler/loca.js';
import { compilerTestCallbacks, loadSectionFixture, withOffset } from './helpers/index.js';
import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { KMXPlus } from '@keymanapp/common-types';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import Loca = KMXPlus.Loca;
import { withOffset } from '@keymanapp/developer-utils';
describe('loca', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while

View file

@ -2,8 +2,7 @@ import 'mocha';
import {expect} from 'chai';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
import { CompilerErrorNamespace, CompilerEvent } from '@keymanapp/developer-utils';
import { withOffset } from './helpers/index.js';
import { CompilerErrorNamespace, CompilerEvent, withOffset } from '@keymanapp/developer-utils';
describe('LdmlCompilerMessages', function () {
it('should have a valid LdmlCompilerMessages object', function() {

View file

@ -1,13 +1,13 @@
import 'mocha';
import {assert} from 'chai';
import { MetaCompiler } from '../src/compiler/meta.js';
import { compilerTestCallbacks, loadSectionFixture, withOffset } from './helpers/index.js';
import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { KMXPlus } from '@keymanapp/common-types';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import KeyboardSettings = KMXPlus.KeyboardSettings;
import Meta = KMXPlus.Meta;
import { LDMLKeyboard } from '@keymanapp/developer-utils';
import { LDMLKeyboard, withOffset } from '@keymanapp/developer-utils';
describe('meta', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while

View file

@ -84,11 +84,14 @@ function do_bundle() {
mkdir -p build/dist
node build-bundler.js
sentry-cli sourcemaps inject \
--org keyman \
--project keyman-developer \
--release "$KEYMAN_VERSION_GIT_TAG" \
build/dist/ "${SOURCEMAP_PATHS[@]}"
if builder_is_ci_build && builder_is_ci_build_level_release; then
# Only inject sourcemaps for release builds
sentry-cli sourcemaps inject \
--org keyman \
--project keyman-developer \
--release "$KEYMAN_VERSION_GIT_TAG" \
build/dist/ "${SOURCEMAP_PATHS[@]}"
fi
# Manually copy over kmcmplib module
cp ../kmc-kmn/build/src/import/kmcmplib/wasm-host.wasm build/dist/

View file

@ -33,7 +33,7 @@ function do_build() {
tds2dbg "$WIN32_TARGET"
cp "$WIN32_TARGET" "$DEVELOPER_PROGRAM"
cp "$WIN32_TARGET_PATH/kmconvert.dbg" "$DEVELOPER_DEBUGPATH"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmconvert.dbg" "$DEVELOPER_DEBUGPATH"
rm -rf "$DEVELOPER_PROGRAM/projects/templates"
mkdir -p "$DEVELOPER_PROGRAM/projects/templates"

View file

@ -32,9 +32,7 @@ function do_build() {
tds2dbg "$WIN32_TARGET"
cp "$WIN32_TARGET" "$DEVELOPER_PROGRAM"
if [[ -f "$WIN32_TARGET_PATH/kmdbrowserhost.dbg" ]]; then
cp "$WIN32_TARGET_PATH/kmdbrowserhost.dbg" "$DEVELOPER_DEBUGPATH"
fi
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmdbrowserhost.dbg" "$DEVELOPER_DEBUGPATH"
}
function do_publish() {

View file

@ -33,7 +33,7 @@ function do_build() {
build_version.res
vs_msbuild kmdecomp.sln //t:Build "//p:Platform=Win32"
cp "$WIN32_TARGET" "$DEVELOPER_PROGRAM"
cp "$WIN32_TARGET_PATH/kmdecomp.pdb" "$DEVELOPER_DEBUGPATH"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmdecomp.pdb" "$DEVELOPER_DEBUGPATH"
}
function do_publish() {

View file

@ -32,11 +32,11 @@ function do_build() {
build_version.res
vs_msbuild imsample.sln //t:Build "//p:Platform=Win32"
cp "$WIN32_TARGET" "$DEVELOPER_PROGRAM"
cp "$WIN32_TARGET_PATH/imsample.pdb" "$DEVELOPER_DEBUGPATH"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/imsample.pdb" "$DEVELOPER_DEBUGPATH"
vs_msbuild imsample.sln //t:Build "//p:Platform=x64"
cp "$X64_TARGET" "$DEVELOPER_PROGRAM"
cp "$X64_TARGET_PATH/imsample.x64.pdb" "$DEVELOPER_DEBUGPATH"
builder_if_release_build_level cp "$X64_TARGET_PATH/imsample.x64.pdb" "$DEVELOPER_DEBUGPATH"
}
# TODO

View file

@ -33,13 +33,8 @@ function do_build() {
sentrytool_delphiprep "$WIN32_TARGET" setup.dpr
tds2dbg "$WIN32_TARGET"
cp "$WIN32_TARGET" "$DEVELOPER_PROGRAM"
if [[ -f "$WIN32_TARGET_PATH/setup.dbg" ]]; then
cp "$WIN32_TARGET_PATH/setup.dbg" "$DEVELOPER_DEBUGPATH/devsetup.dbg"
fi
rm -f "$WIN32_TARGET_PATH/devsetup.dbg"
if [[ -f "$WIN32_TARGET_PATH/setup.dbg" ]]; then
mv "$WIN32_TARGET_PATH/setup.dbg" "$WIN32_TARGET_PATH/devsetup.dbg"
fi
builder_if_release_build_level cp "$WIN32_TARGET_PATH/setup.dbg" "$DEVELOPER_DEBUGPATH/devsetup.dbg"
builder_if_release_build_level mv "$WIN32_TARGET_PATH/setup.dbg" "$WIN32_TARGET_PATH/devsetup.dbg"
}
function do_publish() {

View file

@ -76,11 +76,10 @@ function do_build() {
cp kmlmp.cmd "$DEVELOPER_PROGRAM"
cp kmc.cmd "$DEVELOPER_PROGRAM"
cp "$KEYMAN_ROOT/core/build/x86/$TARGET_PATH/src/$KEYMANCORE_DLL" "$DEVELOPER_PROGRAM"
if [[ -f "$WIN32_TARGET_PATH/tike.dbg" ]]; then
cp "$WIN32_TARGET_PATH/tike.dbg" "$DEVELOPER_DEBUGPATH"
fi
builder_if_release_build_level cp "$WIN32_TARGET_PATH/tike.dbg" "$DEVELOPER_DEBUGPATH"
cp "$KEYMAN_ROOT/core/build/x86/$TARGET_PATH/src/$KEYMANCORE_DLL" "$WIN32_TARGET_PATH"
cp "$KEYMAN_ROOT/core/build/x86/$TARGET_PATH/src/$KEYMANCORE_PDB" "$WIN32_TARGET_PATH"
builder_if_release_build_level cp "$KEYMAN_ROOT/core/build/x86/$TARGET_PATH/src/$KEYMANCORE_PDB" "$WIN32_TARGET_PATH"
cp "$KEYMAN_ROOT/common/windows/delphi/ext/sentry/sentry.dll" "$DEVELOPER_PROGRAM/"
cp "$KEYMAN_ROOT/common/windows/delphi/ext/sentry/sentry.x64.dll" "$DEVELOPER_PROGRAM/"

141
docs/build-bot.md Normal file
View file

@ -0,0 +1,141 @@
# Controlling builds in CI with the Keyman build bot
The Keyman CI test infrastructure can be controlled by use of a `Build-bot:`
commit trailer, or a PR body `Build-bot:` trailer. This allows you to specify
what is built for any given platform. The primary purpose of the build bot is to
reduce the build agent load, but it can also be used to ensure that specific
artifacts are available for the purposes of testing.
The Keyman build bot is only available for test builds on pull requests. It is
not used for any other builds, either test builds on target branches (master,
beta, stable-x.y), or for release builds.
The default set of platform builds is determined by the files touched in the
pull request; see /resources/build/trigger-definitions.inc.sh. The default build
level for this set is 'build' (see [Build Level] section). This is known as the
'build set'.
You may choose to increase or decrease what is built -- for example, for a
documentation-only change, you may decide that nothing needs to be built at all,
with:
```
Build-bot: skip
```
Or you may want to ensure that an artifact is built for Windows:
```
Build-bot: release windows
```
## Build Level
The build level specifies what we want to be run for a test build on a PR.
There is a bit of nomenclature overlap with a 'release build'. A 'release'
buildLevel for a 'test build' is roughly equivalent to what is performed in a
'release build', however, a test build is only ever uploaded to test endpoints
(i.e. TestFlight, Play Store 'test' streams), and never to *.keyman.com, or to
other release distribution endpoints.
The build level is controlled by the Build-bot commit trailer and PR body
Build-bot/Test-bot trailers. The default build level will be 'build'.
For target branch builds, the build level will always be 'build', and Build-bot:
commit trailers are ignored.
### 'skip' build level
Don't do a build at all. This would be appropriate for documentation PRs, for
example, or changes only to comments in source files.
### 'build' build level
Build the code and run unit tests, but don't create artifacts. What this looks
like will vary from platform to platform, but there are some common things we
won't do:
* we won't upload artifacts to TeamCity or *.keyman.com
* we won't upload artifacts to any endpoint such as Play Store
* we won't upload symbols to Sentry
* we won't codesign
However, we _will_ still build an installer (skipping codesigning), as this is
part of the 'build' buildLevel rather than the 'release' buildLevel. The
installer will be thrown away for 'build' build level -- it will not be
available for download as an artifact.
For a platform-specific example, on macOS we will also skip notarizing, as this
is costly and depends on external network resources, making it fragile.
### 'release' build level
A full test build will be run, roughly equivalent to a release build. We will do
the following steps:
* codesign (many platforms) and notarization (macOS)
* upload artifacts to TeamCity (all platforms)
* upload builds to TestFlight / Play Store 'test' endpoints (iOS/Android)
* upload symbols to Sentry
For a 'release' build level:
* we won't upload artifacts to *.keyman.com
* we won't upload artifacts to any release endpoint such as Debian,
packages.sil.org, etc, or to the release areas for Play Store or App Store
(uploading to *.keyman.com and to release endpoints happens in the release TC build config/GHA)
## Controlling the build bot with trailers
The build bot respects commit trailers and trailers in the PR body. The commands
are cumulative and applied in order; the Build-bot trailer in the PR body is
applied after any trailers in commit messages.
If no platform is specified, then the command applies to all platforms in the
current build set, overriding any previous Build-bot commands. Any platforms not
in the current build set will remain 'skipped'.
If a platform is specified, then the command will apply only to that platform,
and the platform will be added to the build set if not already present (and
thus, subsequent Build-bot commands will be applied to this new platform as
well).
It is important to note that the Build-bot trailers are read when builds are
triggered, which happens within 2 minutes of a PR being opened or commits being
pushed. Editing the PR body after the build trigger has run will have no effect
on existing builds, until another commit is pushed, or the test build trigger is
run manually from TeamCity. Thus, do not rely on editing the PR body after PR
creation to control the build bot; either include your build bot trailers in
commit messages, or include them in the PR body submitted when creating the PR.
## Interactions with Test Bot
The Build-bot has limited interactions with the Keyman test bot (aka
keymanapp-test-bot): if a 'User Testing' section is found in the PR body, the
default build level will be upgraded from 'build' to 'release'.
Build bot trailers found in the commits or in the PR body are applied after the
test bot command.
WARNING: The build bot does not check PR comments for Test-bot commands.
## Example Build-bot interactions
Say we have a PR that touches `/android/build.sh`. The default build set will be
`(android:build)`.
The PR body has a User Testing section: `# User Testing`. The build set is
upgraded to: `(android:release)`.
The first commit includes a Build-bot command: `Build-bot: build ios`. The build
set is now `(android:release ios:build)`.
In a subsequent commit, the PR author decides that nothing needs to be built,
after all: `Build-bot: skip`. The build set is now `(android:skip ios:skip)`.
Note that other platforms are still 'skip' but not included in the build set.
Finally, the PR author pushes another commit, with `Build-bot: release windows`.
The build set is now: `(android:skip ios:skip windows:release)`.
[Build Level]: #Build_Level

View file

@ -105,7 +105,7 @@ function copy-installer() {
cp firstvoices.msi "$KEYMAN_ROOT/windows/release/${KEYMAN_VERSION}/firstvoices.msi"
cp firstvoices.exe "$KEYMAN_ROOT/windows/release/${KEYMAN_VERSION}/firstvoices-${KEYMAN_VERSION}.exe"
verify-installer-signatures
builder_if_release_build_level verify-installer-signatures
}
function verify-installer-signatures() {

View file

@ -15,6 +15,7 @@ builder_parse "$@"
if builder_start_action test; then
./build/test/test.sh
./build/test/build-bot/trigger-build-bot.test.sh
./build/publish-minimum-versions.sh test
builder_finish_action success test
fi

View file

@ -158,7 +158,7 @@ if [ "$action" == "commit" ]; then
# Trigger builds for the previous version on TeamCity and GitHub
#
triggerBuilds
triggerReleaseBuilds
#
# Now, create the PR on GitHub which will be merged when ready

View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
builder_describe "Test the pr-build-status.yml GHA" test
builder_parse "$@"
# TODO: consider generating the .yml from here?
builder_run_action test npm test

View file

@ -0,0 +1,384 @@
[
{
"id": 44090590145,
"name": "build",
"node_id": "CR_kwDOAY2xT88AAAAKRAEDwQ",
"head_sha": "11a558e926370253db37026c95b7ff5a6aa1e8fc",
"external_id": "389dd74c-044c-5be6-a27d-4b4819c2ff55",
"url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44090590145",
"html_url": "https://github.com/keymanapp/keyman/actions/runs/15648814574/job/44090590145",
"details_url": "https://github.com/keymanapp/keyman/actions/runs/15648814574/job/44090590145",
"status": "completed",
"conclusion": "skipped",
"started_at": "2025-06-14T05:23:18Z",
"completed_at": "2025-06-14T05:23:18Z",
"output": {
"title": null,
"summary": null,
"text": null,
"annotations_count": 0,
"annotations_url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44090590145/annotations"
},
"check_suite": {
"id": 40118324763
},
"app": {
"id": 15368,
"client_id": "Iv1.05c79e9ad1f6bdfa",
"slug": "github-actions",
"node_id": "MDM6QXBwMTUzNjg=",
"owner": {
"login": "github",
"id": 9919,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjk5MTk=",
"avatar_url": "https://avatars.githubusercontent.com/u/9919?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github",
"html_url": "https://github.com/github",
"followers_url": "https://api.github.com/users/github/followers",
"following_url": "https://api.github.com/users/github/following{/other_user}",
"gists_url": "https://api.github.com/users/github/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github/subscriptions",
"organizations_url": "https://api.github.com/users/github/orgs",
"repos_url": "https://api.github.com/users/github/repos",
"events_url": "https://api.github.com/users/github/events{/privacy}",
"received_events_url": "https://api.github.com/users/github/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"name": "GitHub Actions",
"description": "Automate your workflow from idea to production",
"external_url": "https://help.github.com/en/actions",
"html_url": "https://github.com/apps/github-actions",
"created_at": "2018-07-30T09:30:17Z",
"updated_at": "2025-03-07T16:35:00Z",
"permissions": {
"actions": "write",
"administration": "read",
"attestations": "write",
"checks": "write",
"contents": "write",
"deployments": "write",
"discussions": "write",
"issues": "write",
"merge_queues": "write",
"metadata": "read",
"models": "read",
"packages": "write",
"pages": "write",
"pull_requests": "write",
"repository_hooks": "write",
"repository_projects": "write",
"security_events": "write",
"statuses": "write",
"vulnerability_alerts": "read"
},
"events": [
"branch_protection_rule",
"check_run",
"check_suite",
"create",
"delete",
"deployment",
"deployment_status",
"discussion",
"discussion_comment",
"fork",
"gollum",
"issues",
"issue_comment",
"label",
"merge_group",
"milestone",
"page_build",
"project",
"project_card",
"project_column",
"public",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
"push",
"registry_package",
"release",
"repository",
"repository_dispatch",
"status",
"watch",
"workflow_dispatch",
"workflow_run"
]
},
"pull_requests": [
{
"url": "https://api.github.com/repos/keymanapp/keyman/pulls/14196",
"id": 2591938321,
"number": 14196,
"head": {
"ref": "maint/developer/support-buildLevel",
"sha": "825f7d2df0069fa56a8f2a534b42a49ee1f21ca1",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
},
"base": {
"ref": "maint/common/pr-build-bot",
"sha": "81c941313bcccd8405364731ce61672588285d5d",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
}
}
]
},
{
"id": 44090590129,
"name": "triage",
"node_id": "CR_kwDOAY2xT88AAAAKRAEDsQ",
"head_sha": "11a558e926370253db37026c95b7ff5a6aa1e8fc",
"external_id": "d467db85-960e-541c-a8c1-5f09c33904f5",
"url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44090590129",
"html_url": "https://github.com/keymanapp/keyman/actions/runs/15648814560/job/44090590129",
"details_url": "https://github.com/keymanapp/keyman/actions/runs/15648814560/job/44090590129",
"status": "completed",
"conclusion": "success",
"started_at": "2025-06-14T05:23:21Z",
"completed_at": "2025-06-14T05:23:29Z",
"output": {
"title": null,
"summary": null,
"text": null,
"annotations_count": 0,
"annotations_url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44090590129/annotations"
},
"check_suite": {
"id": 40118324747
},
"app": {
"id": 15368,
"client_id": "Iv1.05c79e9ad1f6bdfa",
"slug": "github-actions",
"node_id": "MDM6QXBwMTUzNjg=",
"owner": {
"login": "github",
"id": 9919,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjk5MTk=",
"avatar_url": "https://avatars.githubusercontent.com/u/9919?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github",
"html_url": "https://github.com/github",
"followers_url": "https://api.github.com/users/github/followers",
"following_url": "https://api.github.com/users/github/following{/other_user}",
"gists_url": "https://api.github.com/users/github/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github/subscriptions",
"organizations_url": "https://api.github.com/users/github/orgs",
"repos_url": "https://api.github.com/users/github/repos",
"events_url": "https://api.github.com/users/github/events{/privacy}",
"received_events_url": "https://api.github.com/users/github/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"name": "GitHub Actions",
"description": "Automate your workflow from idea to production",
"external_url": "https://help.github.com/en/actions",
"html_url": "https://github.com/apps/github-actions",
"created_at": "2018-07-30T09:30:17Z",
"updated_at": "2025-03-07T16:35:00Z",
"permissions": {
"actions": "write",
"administration": "read",
"attestations": "write",
"checks": "write",
"contents": "write",
"deployments": "write",
"discussions": "write",
"issues": "write",
"merge_queues": "write",
"metadata": "read",
"models": "read",
"packages": "write",
"pages": "write",
"pull_requests": "write",
"repository_hooks": "write",
"repository_projects": "write",
"security_events": "write",
"statuses": "write",
"vulnerability_alerts": "read"
},
"events": [
"branch_protection_rule",
"check_run",
"check_suite",
"create",
"delete",
"deployment",
"deployment_status",
"discussion",
"discussion_comment",
"fork",
"gollum",
"issues",
"issue_comment",
"label",
"merge_group",
"milestone",
"page_build",
"project",
"project_card",
"project_column",
"public",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
"push",
"registry_package",
"release",
"repository",
"repository_dispatch",
"status",
"watch",
"workflow_dispatch",
"workflow_run"
]
},
"pull_requests": [
{
"url": "https://api.github.com/repos/keymanapp/keyman/pulls/14196",
"id": 2591938321,
"number": 14196,
"head": {
"ref": "maint/developer/support-buildLevel",
"sha": "825f7d2df0069fa56a8f2a534b42a49ee1f21ca1",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
},
"base": {
"ref": "maint/common/pr-build-bot",
"sha": "81c941313bcccd8405364731ce61672588285d5d",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
}
}
]
},
{
"id": 44090589932,
"name": "GitGuardian Security Checks",
"node_id": "CR_kwDOAY2xT88AAAAKRAEC7A",
"head_sha": "11a558e926370253db37026c95b7ff5a6aa1e8fc",
"external_id": "",
"url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44090589932",
"html_url": "https://github.com/keymanapp/keyman/runs/44090589932",
"details_url": "https://dashboard.gitguardian.com",
"status": "completed",
"conclusion": "success",
"started_at": "2025-06-14T05:23:17Z",
"completed_at": "2025-06-14T05:23:20Z",
"output": {
"title": "No secrets detected ✅",
"summary": "**1** commit was scanned without uncovering any secrets.\n",
"text": "Commit scanned: **1**\n\n\n\n- Pull request #14196: `maint/developer/support-buildLevel` 👉 `maint/common/pr-build-bot`\n \n\n🦉 [GitGuardian](https://dashboard.gitguardian.com/auth/login/?utm_medium=checkruns&amp;utm_source=github&amp;utm_campaign=cr1) detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.<br/>\n",
"annotations_count": 0,
"annotations_url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44090589932/annotations"
},
"check_suite": {
"id": 40118324322
},
"app": {
"id": 46505,
"client_id": "Iv1.ec3c001966b4cc5a",
"slug": "gitguardian",
"node_id": "MDM6QXBwNDY1MDU=",
"owner": {
"login": "GitGuardian",
"id": 27360172,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjI3MzYwMTcy",
"avatar_url": "https://avatars.githubusercontent.com/u/27360172?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/GitGuardian",
"html_url": "https://github.com/GitGuardian",
"followers_url": "https://api.github.com/users/GitGuardian/followers",
"following_url": "https://api.github.com/users/GitGuardian/following{/other_user}",
"gists_url": "https://api.github.com/users/GitGuardian/gists{/gist_id}",
"starred_url": "https://api.github.com/users/GitGuardian/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/GitGuardian/subscriptions",
"organizations_url": "https://api.github.com/users/GitGuardian/orgs",
"repos_url": "https://api.github.com/users/GitGuardian/repos",
"events_url": "https://api.github.com/users/GitGuardian/events{/privacy}",
"received_events_url": "https://api.github.com/users/GitGuardian/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"name": "GitGuardian",
"description": "# 🦉 What is GitGuardian?\r\n\r\nGitGuardian Secrets Detection detects and fixes vulnerabilities in source code at every step of the software development lifecycle, covering 350+ types of secrets like API keys, database connection strings, private keys, certificates, and more. The platforms policy engine enables security teams to monitor and enforce rules across their VCS, DevOps tools, and infrastructure-as-code configurations. Our automated remediation playbooks and collaboration features bring security and development teams together to resolve incidents fast and in full.\r\n\r\n## 1. Scan your codebase for 350+ types of secrets\r\nGitGuardian scans your GitHub repositories and raises alerts only for critical secrets, such as API keys or other credentials. At scale, GitGuardians detection algorithm has been battle-tested on over three years of activity in all public GitHub repositories totaling over 1 billion scanned commits!\r\n\r\n## 2. Quickly remediate your hard-coded secrets\r\nIf you ever experience a leak involving a credential, we have a complete remediation guide used by 100k+ developers each year. Well show you how to revoke the secret and remove it from your git history.\r\n\r\n## 3. Prevent secrets from reaching GitHub\r\nInstall ggshield, the GitGuardian CLI, and add secrets detection to your local development workflow using pre-commit and pre-push git hooks integrations.\r\n\r\n# 🙋‍♂️ FAQ\r\n\r\n**What is your pricing?**\r\nGitGuardian is free for teams under 25 developers and offers a 30-day trial for larger teams.\r\n\r\n**How can I be sure that GitGuardian wont raise too many false positives?**\r\nWe have scanned billions of commits, sent millions of alerts since 2018, and integrated each feedback to improve our algorithm. Our alerts currently receive 91% “true positive” feedback from developers.\r\n\r\n**My repositories are private; why should I install automated secret detection?**\r\nImagine if there were a plain text file with all your credit card numbers inside, you wouldnt put this file inside your companys git repository. Secrets are just as sensitive and should be handled with special care.\r\n\r\n**Is GitGuardian available to be installed on-premise?**\r\nYes, you can contact one of our security specialists to look over the possibility of installing GitGuardian on-premise on your repositories.\r\n\r\n# ⚒️ Installation notes \r\n\r\nYou should install GitGuardian directly through your [GitGuardian](https://dashboard.gitguardian.com/) workspace on the Integration settings page. \r\n\r\nSo that you know, your GitHub organization or GitHub account can only be associated with a single GitGuardian workspace.\r\n\r\n# 👋 Support\r\n\r\nIf you experience any difficulties or have any questions, please reach out to us by email ([support@gitguardian.com](mailto:support@gitguardian.com)).",
"external_url": "https://dashboard.gitguardian.com",
"html_url": "https://github.com/apps/gitguardian",
"created_at": "2019-11-12T15:44:31Z",
"updated_at": "2023-07-18T09:06:22Z",
"permissions": {
"checks": "write",
"contents": "read",
"emails": "read",
"issues": "write",
"members": "read",
"metadata": "read",
"organization_hooks": "write",
"pull_requests": "write"
},
"events": [
"check_run",
"check_suite",
"commit_comment",
"create",
"delete",
"organization",
"public",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
"push",
"repository"
]
},
"pull_requests": [
{
"url": "https://api.github.com/repos/keymanapp/keyman/pulls/14196",
"id": 2591938321,
"number": 14196,
"head": {
"ref": "maint/developer/support-buildLevel",
"sha": "825f7d2df0069fa56a8f2a534b42a49ee1f21ca1",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
},
"base": {
"ref": "maint/common/pr-build-bot",
"sha": "81c941313bcccd8405364731ce61672588285d5d",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
}
}
]
}
]

View file

@ -0,0 +1,246 @@
[
{
"id": 44134134750,
"name": "triage",
"node_id": "CR_kwDOAY2xT88AAAAKRplz3g",
"head_sha": "825f7d2df0069fa56a8f2a534b42a49ee1f21ca1",
"external_id": "aece869f-5587-5930-867f-8649375f3d24",
"url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44134134750",
"html_url": "https://github.com/keymanapp/keyman/actions/runs/15667765443/job/44134134750",
"details_url": "https://github.com/keymanapp/keyman/actions/runs/15667765443/job/44134134750",
"status": "completed",
"conclusion": "success",
"started_at": "2025-06-15T21:54:36Z",
"completed_at": "2025-06-15T21:54:41Z",
"output": {
"title": null,
"summary": null,
"text": null,
"annotations_count": 0,
"annotations_url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44134134750/annotations"
},
"check_suite": {
"id": 40159371625
},
"app": {
"id": 15368,
"client_id": "Iv1.05c79e9ad1f6bdfa",
"slug": "github-actions",
"node_id": "MDM6QXBwMTUzNjg=",
"owner": {
"login": "github",
"id": 9919,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjk5MTk=",
"avatar_url": "https://avatars.githubusercontent.com/u/9919?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github",
"html_url": "https://github.com/github",
"followers_url": "https://api.github.com/users/github/followers",
"following_url": "https://api.github.com/users/github/following{/other_user}",
"gists_url": "https://api.github.com/users/github/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github/subscriptions",
"organizations_url": "https://api.github.com/users/github/orgs",
"repos_url": "https://api.github.com/users/github/repos",
"events_url": "https://api.github.com/users/github/events{/privacy}",
"received_events_url": "https://api.github.com/users/github/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"name": "GitHub Actions",
"description": "Automate your workflow from idea to production",
"external_url": "https://help.github.com/en/actions",
"html_url": "https://github.com/apps/github-actions",
"created_at": "2018-07-30T09:30:17Z",
"updated_at": "2025-03-07T16:35:00Z",
"permissions": {
"actions": "write",
"administration": "read",
"attestations": "write",
"checks": "write",
"contents": "write",
"deployments": "write",
"discussions": "write",
"issues": "write",
"merge_queues": "write",
"metadata": "read",
"models": "read",
"packages": "write",
"pages": "write",
"pull_requests": "write",
"repository_hooks": "write",
"repository_projects": "write",
"security_events": "write",
"statuses": "write",
"vulnerability_alerts": "read"
},
"events": [
"branch_protection_rule",
"check_run",
"check_suite",
"create",
"delete",
"deployment",
"deployment_status",
"discussion",
"discussion_comment",
"fork",
"gollum",
"issues",
"issue_comment",
"label",
"merge_group",
"milestone",
"page_build",
"project",
"project_card",
"project_column",
"public",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
"push",
"registry_package",
"release",
"repository",
"repository_dispatch",
"status",
"watch",
"workflow_dispatch",
"workflow_run"
]
},
"pull_requests": [
{
"url": "https://api.github.com/repos/keymanapp/keyman/pulls/14196",
"id": 2591938321,
"number": 14196,
"head": {
"ref": "maint/developer/support-buildLevel",
"sha": "825f7d2df0069fa56a8f2a534b42a49ee1f21ca1",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
},
"base": {
"ref": "maint/common/pr-build-bot",
"sha": "81c941313bcccd8405364731ce61672588285d5d",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
}
}
]
},
{
"id": 44134134737,
"name": "GitGuardian Security Checks",
"node_id": "CR_kwDOAY2xT88AAAAKRplz0Q",
"head_sha": "825f7d2df0069fa56a8f2a534b42a49ee1f21ca1",
"external_id": "",
"url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44134134737",
"html_url": "https://github.com/keymanapp/keyman/runs/44134134737",
"details_url": "https://dashboard.gitguardian.com",
"status": "completed",
"conclusion": "success",
"started_at": "2025-06-15T21:54:32Z",
"completed_at": "2025-06-15T21:54:44Z",
"output": {
"title": "No secrets detected ✅",
"summary": "**5** commits were scanned without uncovering any secrets.\n",
"text": "Commits scanned: **5**\n\n\n\n- Pull request #14196: `maint/developer/support-buildLevel` 👉 `maint/common/pr-build-bot`\n \n\n🦉 [GitGuardian](https://dashboard.gitguardian.com/auth/login/?utm_medium=checkruns&amp;utm_source=github&amp;utm_campaign=cr1) detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.<br/>\n",
"annotations_count": 0,
"annotations_url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44134134737/annotations"
},
"check_suite": {
"id": 40159371205
},
"app": {
"id": 46505,
"client_id": "Iv1.ec3c001966b4cc5a",
"slug": "gitguardian",
"node_id": "MDM6QXBwNDY1MDU=",
"owner": {
"login": "GitGuardian",
"id": 27360172,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjI3MzYwMTcy",
"avatar_url": "https://avatars.githubusercontent.com/u/27360172?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/GitGuardian",
"html_url": "https://github.com/GitGuardian",
"followers_url": "https://api.github.com/users/GitGuardian/followers",
"following_url": "https://api.github.com/users/GitGuardian/following{/other_user}",
"gists_url": "https://api.github.com/users/GitGuardian/gists{/gist_id}",
"starred_url": "https://api.github.com/users/GitGuardian/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/GitGuardian/subscriptions",
"organizations_url": "https://api.github.com/users/GitGuardian/orgs",
"repos_url": "https://api.github.com/users/GitGuardian/repos",
"events_url": "https://api.github.com/users/GitGuardian/events{/privacy}",
"received_events_url": "https://api.github.com/users/GitGuardian/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"name": "GitGuardian",
"description": "# 🦉 What is GitGuardian?\r\n\r\nGitGuardian Secrets Detection detects and fixes vulnerabilities in source code at every step of the software development lifecycle, covering 350+ types of secrets like API keys, database connection strings, private keys, certificates, and more. The platforms policy engine enables security teams to monitor and enforce rules across their VCS, DevOps tools, and infrastructure-as-code configurations. Our automated remediation playbooks and collaboration features bring security and development teams together to resolve incidents fast and in full.\r\n\r\n## 1. Scan your codebase for 350+ types of secrets\r\nGitGuardian scans your GitHub repositories and raises alerts only for critical secrets, such as API keys or other credentials. At scale, GitGuardians detection algorithm has been battle-tested on over three years of activity in all public GitHub repositories totaling over 1 billion scanned commits!\r\n\r\n## 2. Quickly remediate your hard-coded secrets\r\nIf you ever experience a leak involving a credential, we have a complete remediation guide used by 100k+ developers each year. Well show you how to revoke the secret and remove it from your git history.\r\n\r\n## 3. Prevent secrets from reaching GitHub\r\nInstall ggshield, the GitGuardian CLI, and add secrets detection to your local development workflow using pre-commit and pre-push git hooks integrations.\r\n\r\n# 🙋‍♂️ FAQ\r\n\r\n**What is your pricing?**\r\nGitGuardian is free for teams under 25 developers and offers a 30-day trial for larger teams.\r\n\r\n**How can I be sure that GitGuardian wont raise too many false positives?**\r\nWe have scanned billions of commits, sent millions of alerts since 2018, and integrated each feedback to improve our algorithm. Our alerts currently receive 91% “true positive” feedback from developers.\r\n\r\n**My repositories are private; why should I install automated secret detection?**\r\nImagine if there were a plain text file with all your credit card numbers inside, you wouldnt put this file inside your companys git repository. Secrets are just as sensitive and should be handled with special care.\r\n\r\n**Is GitGuardian available to be installed on-premise?**\r\nYes, you can contact one of our security specialists to look over the possibility of installing GitGuardian on-premise on your repositories.\r\n\r\n# ⚒️ Installation notes \r\n\r\nYou should install GitGuardian directly through your [GitGuardian](https://dashboard.gitguardian.com/) workspace on the Integration settings page. \r\n\r\nSo that you know, your GitHub organization or GitHub account can only be associated with a single GitGuardian workspace.\r\n\r\n# 👋 Support\r\n\r\nIf you experience any difficulties or have any questions, please reach out to us by email ([support@gitguardian.com](mailto:support@gitguardian.com)).",
"external_url": "https://dashboard.gitguardian.com",
"html_url": "https://github.com/apps/gitguardian",
"created_at": "2019-11-12T15:44:31Z",
"updated_at": "2023-07-18T09:06:22Z",
"permissions": {
"checks": "write",
"contents": "read",
"emails": "read",
"issues": "write",
"members": "read",
"metadata": "read",
"organization_hooks": "write",
"pull_requests": "write"
},
"events": [
"check_run",
"check_suite",
"commit_comment",
"create",
"delete",
"organization",
"public",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
"push",
"repository"
]
},
"pull_requests": [
{
"url": "https://api.github.com/repos/keymanapp/keyman/pulls/14196",
"id": 2591938321,
"number": 14196,
"head": {
"ref": "maint/developer/support-buildLevel",
"sha": "825f7d2df0069fa56a8f2a534b42a49ee1f21ca1",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
},
"base": {
"ref": "maint/common/pr-build-bot",
"sha": "81c941313bcccd8405364731ce61672588285d5d",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
}
}
]
}
]

View file

@ -0,0 +1,246 @@
[
{
"id": 44131811673,
"name": "GitGuardian Security Checks",
"node_id": "CR_kwDOAY2xT88AAAAKRnYBWQ",
"head_sha": "870d37f56b489ef5067b4352a06505369d9038e0",
"external_id": "",
"url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44131811673",
"html_url": "https://github.com/keymanapp/keyman/runs/44131811673",
"details_url": "https://dashboard.gitguardian.com",
"status": "completed",
"conclusion": "success",
"started_at": "2025-06-15T19:40:57Z",
"completed_at": "2025-06-15T19:41:28Z",
"output": {
"title": "No secrets detected ✅",
"summary": "**2** commits were scanned without uncovering any secrets.\n",
"text": "Commits scanned: **2**\n\n\n\n- Pull request #14196: `maint/developer/support-buildLevel` 👉 `maint/common/pr-build-bot`\n \n\n🦉 [GitGuardian](https://dashboard.gitguardian.com/auth/login/?utm_medium=checkruns&amp;utm_source=github&amp;utm_campaign=cr1) detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.<br/>\n",
"annotations_count": 0,
"annotations_url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44131811673/annotations"
},
"check_suite": {
"id": 40157100674
},
"app": {
"id": 46505,
"client_id": "Iv1.ec3c001966b4cc5a",
"slug": "gitguardian",
"node_id": "MDM6QXBwNDY1MDU=",
"owner": {
"login": "GitGuardian",
"id": 27360172,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjI3MzYwMTcy",
"avatar_url": "https://avatars.githubusercontent.com/u/27360172?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/GitGuardian",
"html_url": "https://github.com/GitGuardian",
"followers_url": "https://api.github.com/users/GitGuardian/followers",
"following_url": "https://api.github.com/users/GitGuardian/following{/other_user}",
"gists_url": "https://api.github.com/users/GitGuardian/gists{/gist_id}",
"starred_url": "https://api.github.com/users/GitGuardian/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/GitGuardian/subscriptions",
"organizations_url": "https://api.github.com/users/GitGuardian/orgs",
"repos_url": "https://api.github.com/users/GitGuardian/repos",
"events_url": "https://api.github.com/users/GitGuardian/events{/privacy}",
"received_events_url": "https://api.github.com/users/GitGuardian/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"name": "GitGuardian",
"description": "# 🦉 What is GitGuardian?\r\n\r\nGitGuardian Secrets Detection detects and fixes vulnerabilities in source code at every step of the software development lifecycle, covering 350+ types of secrets like API keys, database connection strings, private keys, certificates, and more. The platforms policy engine enables security teams to monitor and enforce rules across their VCS, DevOps tools, and infrastructure-as-code configurations. Our automated remediation playbooks and collaboration features bring security and development teams together to resolve incidents fast and in full.\r\n\r\n## 1. Scan your codebase for 350+ types of secrets\r\nGitGuardian scans your GitHub repositories and raises alerts only for critical secrets, such as API keys or other credentials. At scale, GitGuardians detection algorithm has been battle-tested on over three years of activity in all public GitHub repositories totaling over 1 billion scanned commits!\r\n\r\n## 2. Quickly remediate your hard-coded secrets\r\nIf you ever experience a leak involving a credential, we have a complete remediation guide used by 100k+ developers each year. Well show you how to revoke the secret and remove it from your git history.\r\n\r\n## 3. Prevent secrets from reaching GitHub\r\nInstall ggshield, the GitGuardian CLI, and add secrets detection to your local development workflow using pre-commit and pre-push git hooks integrations.\r\n\r\n# 🙋‍♂️ FAQ\r\n\r\n**What is your pricing?**\r\nGitGuardian is free for teams under 25 developers and offers a 30-day trial for larger teams.\r\n\r\n**How can I be sure that GitGuardian wont raise too many false positives?**\r\nWe have scanned billions of commits, sent millions of alerts since 2018, and integrated each feedback to improve our algorithm. Our alerts currently receive 91% “true positive” feedback from developers.\r\n\r\n**My repositories are private; why should I install automated secret detection?**\r\nImagine if there were a plain text file with all your credit card numbers inside, you wouldnt put this file inside your companys git repository. Secrets are just as sensitive and should be handled with special care.\r\n\r\n**Is GitGuardian available to be installed on-premise?**\r\nYes, you can contact one of our security specialists to look over the possibility of installing GitGuardian on-premise on your repositories.\r\n\r\n# ⚒️ Installation notes \r\n\r\nYou should install GitGuardian directly through your [GitGuardian](https://dashboard.gitguardian.com/) workspace on the Integration settings page. \r\n\r\nSo that you know, your GitHub organization or GitHub account can only be associated with a single GitGuardian workspace.\r\n\r\n# 👋 Support\r\n\r\nIf you experience any difficulties or have any questions, please reach out to us by email ([support@gitguardian.com](mailto:support@gitguardian.com)).",
"external_url": "https://dashboard.gitguardian.com",
"html_url": "https://github.com/apps/gitguardian",
"created_at": "2019-11-12T15:44:31Z",
"updated_at": "2023-07-18T09:06:22Z",
"permissions": {
"checks": "write",
"contents": "read",
"emails": "read",
"issues": "write",
"members": "read",
"metadata": "read",
"organization_hooks": "write",
"pull_requests": "write"
},
"events": [
"check_run",
"check_suite",
"commit_comment",
"create",
"delete",
"organization",
"public",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
"push",
"repository"
]
},
"pull_requests": [
{
"url": "https://api.github.com/repos/keymanapp/keyman/pulls/14196",
"id": 2591938321,
"number": 14196,
"head": {
"ref": "maint/developer/support-buildLevel",
"sha": "825f7d2df0069fa56a8f2a534b42a49ee1f21ca1",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
},
"base": {
"ref": "maint/common/pr-build-bot",
"sha": "81c941313bcccd8405364731ce61672588285d5d",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
}
}
]
},
{
"id": 44131810574,
"name": "triage",
"node_id": "CR_kwDOAY2xT88AAAAKRnX9Dg",
"head_sha": "870d37f56b489ef5067b4352a06505369d9038e0",
"external_id": "3d143feb-b3d7-59b2-ae1b-fe551167c2b4",
"url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44131810574",
"html_url": "https://github.com/keymanapp/keyman/actions/runs/15666730139/job/44131810574",
"details_url": "https://github.com/keymanapp/keyman/actions/runs/15666730139/job/44131810574",
"status": "completed",
"conclusion": "success",
"started_at": "2025-06-15T19:40:57Z",
"completed_at": "2025-06-15T19:41:04Z",
"output": {
"title": null,
"summary": null,
"text": null,
"annotations_count": 0,
"annotations_url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44131810574/annotations"
},
"check_suite": {
"id": 40157101634
},
"app": {
"id": 15368,
"client_id": "Iv1.05c79e9ad1f6bdfa",
"slug": "github-actions",
"node_id": "MDM6QXBwMTUzNjg=",
"owner": {
"login": "github",
"id": 9919,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjk5MTk=",
"avatar_url": "https://avatars.githubusercontent.com/u/9919?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github",
"html_url": "https://github.com/github",
"followers_url": "https://api.github.com/users/github/followers",
"following_url": "https://api.github.com/users/github/following{/other_user}",
"gists_url": "https://api.github.com/users/github/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github/subscriptions",
"organizations_url": "https://api.github.com/users/github/orgs",
"repos_url": "https://api.github.com/users/github/repos",
"events_url": "https://api.github.com/users/github/events{/privacy}",
"received_events_url": "https://api.github.com/users/github/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"name": "GitHub Actions",
"description": "Automate your workflow from idea to production",
"external_url": "https://help.github.com/en/actions",
"html_url": "https://github.com/apps/github-actions",
"created_at": "2018-07-30T09:30:17Z",
"updated_at": "2025-03-07T16:35:00Z",
"permissions": {
"actions": "write",
"administration": "read",
"attestations": "write",
"checks": "write",
"contents": "write",
"deployments": "write",
"discussions": "write",
"issues": "write",
"merge_queues": "write",
"metadata": "read",
"models": "read",
"packages": "write",
"pages": "write",
"pull_requests": "write",
"repository_hooks": "write",
"repository_projects": "write",
"security_events": "write",
"statuses": "write",
"vulnerability_alerts": "read"
},
"events": [
"branch_protection_rule",
"check_run",
"check_suite",
"create",
"delete",
"deployment",
"deployment_status",
"discussion",
"discussion_comment",
"fork",
"gollum",
"issues",
"issue_comment",
"label",
"merge_group",
"milestone",
"page_build",
"project",
"project_card",
"project_column",
"public",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
"push",
"registry_package",
"release",
"repository",
"repository_dispatch",
"status",
"watch",
"workflow_dispatch",
"workflow_run"
]
},
"pull_requests": [
{
"url": "https://api.github.com/repos/keymanapp/keyman/pulls/14196",
"id": 2591938321,
"number": 14196,
"head": {
"ref": "maint/developer/support-buildLevel",
"sha": "825f7d2df0069fa56a8f2a534b42a49ee1f21ca1",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
},
"base": {
"ref": "maint/common/pr-build-bot",
"sha": "81c941313bcccd8405364731ce61672588285d5d",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
}
}
]
}
]

View file

@ -0,0 +1,246 @@
[
{
"id": 44207248981,
"name": "triage",
"node_id": "CR_kwDOAY2xT88AAAAKSvUWVQ",
"head_sha": "8fd5ccc9026b85d6a780d8bcf862380e7a89aefb",
"external_id": "08ccda57-e306-5823-a607-291b5482a6f2",
"url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44207248981",
"html_url": "https://github.com/keymanapp/keyman/actions/runs/15691412642/job/44207248981",
"details_url": "https://github.com/keymanapp/keyman/actions/runs/15691412642/job/44207248981",
"status": "completed",
"conclusion": "success",
"started_at": "2025-06-16T20:38:08Z",
"completed_at": "2025-06-16T20:38:12Z",
"output": {
"title": null,
"summary": null,
"text": null,
"annotations_count": 0,
"annotations_url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44207248981/annotations"
},
"check_suite": {
"id": 40219154773
},
"app": {
"id": 15368,
"client_id": "Iv1.05c79e9ad1f6bdfa",
"slug": "github-actions",
"node_id": "MDM6QXBwMTUzNjg=",
"owner": {
"login": "github",
"id": 9919,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjk5MTk=",
"avatar_url": "https://avatars.githubusercontent.com/u/9919?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github",
"html_url": "https://github.com/github",
"followers_url": "https://api.github.com/users/github/followers",
"following_url": "https://api.github.com/users/github/following{/other_user}",
"gists_url": "https://api.github.com/users/github/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github/subscriptions",
"organizations_url": "https://api.github.com/users/github/orgs",
"repos_url": "https://api.github.com/users/github/repos",
"events_url": "https://api.github.com/users/github/events{/privacy}",
"received_events_url": "https://api.github.com/users/github/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"name": "GitHub Actions",
"description": "Automate your workflow from idea to production",
"external_url": "https://help.github.com/en/actions",
"html_url": "https://github.com/apps/github-actions",
"created_at": "2018-07-30T09:30:17Z",
"updated_at": "2025-03-07T16:35:00Z",
"permissions": {
"actions": "write",
"administration": "read",
"attestations": "write",
"checks": "write",
"contents": "write",
"deployments": "write",
"discussions": "write",
"issues": "write",
"merge_queues": "write",
"metadata": "read",
"models": "read",
"packages": "write",
"pages": "write",
"pull_requests": "write",
"repository_hooks": "write",
"repository_projects": "write",
"security_events": "write",
"statuses": "write",
"vulnerability_alerts": "read"
},
"events": [
"branch_protection_rule",
"check_run",
"check_suite",
"create",
"delete",
"deployment",
"deployment_status",
"discussion",
"discussion_comment",
"fork",
"gollum",
"issues",
"issue_comment",
"label",
"merge_group",
"milestone",
"page_build",
"project",
"project_card",
"project_column",
"public",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
"push",
"registry_package",
"release",
"repository",
"repository_dispatch",
"status",
"watch",
"workflow_dispatch",
"workflow_run"
]
},
"pull_requests": [
{
"url": "https://api.github.com/repos/keymanapp/keyman/pulls/14199",
"id": 2594414200,
"number": 14199,
"head": {
"ref": "maint/resources/14172-pr-build-status-2",
"sha": "c0d28233302cbc22c7d7625dfba5930006ad7d0b",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
},
"base": {
"ref": "maint/developer/support-buildLevel",
"sha": "825f7d2df0069fa56a8f2a534b42a49ee1f21ca1",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
}
}
]
},
{
"id": 44207248005,
"name": "GitGuardian Security Checks",
"node_id": "CR_kwDOAY2xT88AAAAKSvUShQ",
"head_sha": "8fd5ccc9026b85d6a780d8bcf862380e7a89aefb",
"external_id": "",
"url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44207248005",
"html_url": "https://github.com/keymanapp/keyman/runs/44207248005",
"details_url": "https://dashboard.gitguardian.com",
"status": "completed",
"conclusion": "success",
"started_at": "2025-06-16T20:38:03Z",
"completed_at": "2025-06-16T20:38:06Z",
"output": {
"title": "No secrets detected ✅",
"summary": "**8** commits were scanned without uncovering any secrets.\n",
"text": "Commits scanned: **8**\n\n\n\n- Pull request #14199: `maint/resources/14172-pr-build-status-2` 👉 `maint/developer/support-buildLevel`\n \n\n🦉 [GitGuardian](https://dashboard.gitguardian.com/auth/login/?utm_medium=checkruns&amp;utm_source=github&amp;utm_campaign=cr1) detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.<br/>\n",
"annotations_count": 0,
"annotations_url": "https://api.github.com/repos/keymanapp/keyman/check-runs/44207248005/annotations"
},
"check_suite": {
"id": 40219152500
},
"app": {
"id": 46505,
"client_id": "Iv1.ec3c001966b4cc5a",
"slug": "gitguardian",
"node_id": "MDM6QXBwNDY1MDU=",
"owner": {
"login": "GitGuardian",
"id": 27360172,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjI3MzYwMTcy",
"avatar_url": "https://avatars.githubusercontent.com/u/27360172?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/GitGuardian",
"html_url": "https://github.com/GitGuardian",
"followers_url": "https://api.github.com/users/GitGuardian/followers",
"following_url": "https://api.github.com/users/GitGuardian/following{/other_user}",
"gists_url": "https://api.github.com/users/GitGuardian/gists{/gist_id}",
"starred_url": "https://api.github.com/users/GitGuardian/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/GitGuardian/subscriptions",
"organizations_url": "https://api.github.com/users/GitGuardian/orgs",
"repos_url": "https://api.github.com/users/GitGuardian/repos",
"events_url": "https://api.github.com/users/GitGuardian/events{/privacy}",
"received_events_url": "https://api.github.com/users/GitGuardian/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"name": "GitGuardian",
"description": "# 🦉 What is GitGuardian?\r\n\r\nGitGuardian Secrets Detection detects and fixes vulnerabilities in source code at every step of the software development lifecycle, covering 350+ types of secrets like API keys, database connection strings, private keys, certificates, and more. The platforms policy engine enables security teams to monitor and enforce rules across their VCS, DevOps tools, and infrastructure-as-code configurations. Our automated remediation playbooks and collaboration features bring security and development teams together to resolve incidents fast and in full.\r\n\r\n## 1. Scan your codebase for 350+ types of secrets\r\nGitGuardian scans your GitHub repositories and raises alerts only for critical secrets, such as API keys or other credentials. At scale, GitGuardians detection algorithm has been battle-tested on over three years of activity in all public GitHub repositories totaling over 1 billion scanned commits!\r\n\r\n## 2. Quickly remediate your hard-coded secrets\r\nIf you ever experience a leak involving a credential, we have a complete remediation guide used by 100k+ developers each year. Well show you how to revoke the secret and remove it from your git history.\r\n\r\n## 3. Prevent secrets from reaching GitHub\r\nInstall ggshield, the GitGuardian CLI, and add secrets detection to your local development workflow using pre-commit and pre-push git hooks integrations.\r\n\r\n# 🙋‍♂️ FAQ\r\n\r\n**What is your pricing?**\r\nGitGuardian is free for teams under 25 developers and offers a 30-day trial for larger teams.\r\n\r\n**How can I be sure that GitGuardian wont raise too many false positives?**\r\nWe have scanned billions of commits, sent millions of alerts since 2018, and integrated each feedback to improve our algorithm. Our alerts currently receive 91% “true positive” feedback from developers.\r\n\r\n**My repositories are private; why should I install automated secret detection?**\r\nImagine if there were a plain text file with all your credit card numbers inside, you wouldnt put this file inside your companys git repository. Secrets are just as sensitive and should be handled with special care.\r\n\r\n**Is GitGuardian available to be installed on-premise?**\r\nYes, you can contact one of our security specialists to look over the possibility of installing GitGuardian on-premise on your repositories.\r\n\r\n# ⚒️ Installation notes \r\n\r\nYou should install GitGuardian directly through your [GitGuardian](https://dashboard.gitguardian.com/) workspace on the Integration settings page. \r\n\r\nSo that you know, your GitHub organization or GitHub account can only be associated with a single GitGuardian workspace.\r\n\r\n# 👋 Support\r\n\r\nIf you experience any difficulties or have any questions, please reach out to us by email ([support@gitguardian.com](mailto:support@gitguardian.com)).",
"external_url": "https://dashboard.gitguardian.com",
"html_url": "https://github.com/apps/gitguardian",
"created_at": "2019-11-12T15:44:31Z",
"updated_at": "2023-07-18T09:06:22Z",
"permissions": {
"checks": "write",
"contents": "read",
"emails": "read",
"issues": "write",
"members": "read",
"metadata": "read",
"organization_hooks": "write",
"pull_requests": "write"
},
"events": [
"check_run",
"check_suite",
"commit_comment",
"create",
"delete",
"organization",
"public",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
"push",
"repository"
]
},
"pull_requests": [
{
"url": "https://api.github.com/repos/keymanapp/keyman/pulls/14199",
"id": 2594414200,
"number": 14199,
"head": {
"ref": "maint/resources/14172-pr-build-status-2",
"sha": "c0d28233302cbc22c7d7625dfba5930006ad7d0b",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
},
"base": {
"ref": "maint/developer/support-buildLevel",
"sha": "825f7d2df0069fa56a8f2a534b42a49ee1f21ca1",
"repo": {
"id": 26063183,
"url": "https://api.github.com/repos/keymanapp/keyman",
"name": "keyman"
}
}
}
]
}
]

View file

@ -0,0 +1,35 @@
[
{
"url": "https://api.github.com/repos/keymanapp/keyman/statuses/8fd5ccc9026b85d6a780d8bcf862380e7a89aefb",
"avatar_url": "https://avatars.githubusercontent.com/in/133554?v=4",
"id": 37011551277,
"node_id": "SC_kwDOAY2xT88AAAAIng90LQ",
"state": "success",
"description": "User tests are not required",
"target_url": "https://github.com/keymanapp/keyman/pull/14199#issuecomment-2975084272",
"context": "user_testing",
"created_at": "2025-06-16T20:38:04Z",
"updated_at": "2025-06-16T20:38:04Z",
"creator": {
"login": "keymanapp-test-bot[bot]",
"id": 89363325,
"node_id": "MDM6Qm90ODkzNjMzMjU=",
"avatar_url": "https://avatars.githubusercontent.com/in/133554?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/keymanapp-test-bot%5Bbot%5D",
"html_url": "https://github.com/apps/keymanapp-test-bot",
"followers_url": "https://api.github.com/users/keymanapp-test-bot%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/keymanapp-test-bot%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/keymanapp-test-bot%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/keymanapp-test-bot%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/keymanapp-test-bot%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/keymanapp-test-bot%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/keymanapp-test-bot%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/keymanapp-test-bot%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/keymanapp-test-bot%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
}
}
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,20 @@
{
"description": "CHeck pull request build status - development area",
"type": "module",
"dependencies": {
"@actions/core": "^1.9.1",
"@actions/github": "^6.0.1"
},
"devDependencies": {
"mocha": "^11.2.2",
"mocha-teamcity-reporter": "^4.0.0",
"chai": "^5.1.0"
},
"license": "MIT",
"main": "pr-build-status.mjs",
"name": "@keymanapp/pr-build-status",
"private": true,
"scripts": {
"test": "mocha pr-build-status.tests.mjs"
}
}

View file

@ -0,0 +1,188 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Clone of the pr-build-status.yml file for unit testing and development. The
* indented section of this file below is copied verbatim into
* .github/workflows/pr-build-status.yml, and allows us to develop the YAML
* script without introducing a dependency on the repository.
*/
// Copy the indented section into the step for pr-build-status.mjs.
// START OF CLONED SECTION
// This code is copied out of resources/build/pr-build-status/pr-build-status.mjs
// where it is tested. It is copied inline here in order to avoid requiring the
// repository to be checked out, which dramatically reduces the run time of the
// check.
//
// Note: we don't currently look at check runs, only statuses
//
// Verify the following statuses:
// 'user_testing'
// 'API Verification' (github-actions[bot])
//
// At least 1 of the following statuses must be found:
// 'Test*' (keyman-server), e.g. 'Test Build (Keyman)'
// 'Ubuntu Packaging' (github-actions[bot])
//
// Ignore the following statuses:
// check/web/file-size
//
function reduceStatuses(statuses) {
const filtered_statuses = statuses.reduce((o, status) => {
if(status.creator?.login == 'keyman-server' && status.context.startsWith('Test')) {
if(!o[status.context]) o[status.context] = {type: 'build', state: status.state};
} else if(status.creator?.login == 'keymanapp-test-bot[bot]' && status.context == 'user_testing') {
if(!o[status.context]) o[status.context] = {type: 'user-test', state: status.state};;
} else if(status.context == 'API Verification') {
if(!o[status.context]) o[status.context] = {type: 'check', state: status.state};
} else if(status.context == 'Ubuntu Packaging') {
if(!o[status.context]) o[status.context] = {type: 'build', state: status.state};
} else if(status.context == 'check/web/file-size') {
// Ignore check/web/file-size -- we won't block automerge for this at this point
} else {
// We fail with an 'unknown status' response if we get a new status check
// so we can be sure we are not skipping known status checks
o[status.context] = {type: 'unknown', state: status.state};
}
return o;
}, {});
return filtered_statuses;
}
//
// Given the collection of status checks we care about, return
// an aggregate status -- error, failed, pending, or success,
// and a summary description
//
function calculateFinalStatus(filtered_statuses) {
const counts = {};
let hasBuilds = false;
for(const context of Object.keys(filtered_statuses)) {
const { state, type } = filtered_statuses[context];
if(type == 'unknown') {
// We special-case for unknown status checks, and never permit them
return [
'error', `An unknown context ${context} was found, cannot calculate build status.`
];
}
if(type == 'build') {
hasBuilds = true;
}
counts[state] = counts[state] ? counts[state] + 1 : 1;
}
// If we do not have any statuses yet, we wait
if(Object.keys(filtered_statuses).length == 0 || !hasBuilds) {
return ['pending', 'Checks have not yet been triggered ⌛'];
}
const state =
counts.error ? 'error' :
counts.failed ? 'failed' :
counts.pending ? 'pending' :
'success';
let description = ''; //;
function appendDescription(count, state) {
if(!count) return;
if(description != '') description += '; ';
description += `${count} check${count == 1 ? '' : 's'} ${state}`;
}
appendDescription(counts.error, 'in an error state ❌');
appendDescription(counts.failed, 'failed ❌');
appendDescription(counts.pending, 'pending ⌛');
appendDescription(counts.success, 'completed successfully ✅');
return [ state, description ];
}
async function getCommitStatuses(github, owner, repo, sha) {
const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{sha}/statuses', {
owner,
repo,
sha,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
return statuses;
}
async function getCommitCheckRuns(github, owner, repo, sha) {
const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{sha}/check-runs', {
owner,
repo,
sha,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
return statuses;
}
function calculateCheckResult(statuses) {
if(!Array.isArray(statuses)) {
return ['error', 'Failed to retrieve status checks from GitHub ❌'];
}
const filtered_statuses = reduceStatuses(statuses);
const result = calculateFinalStatus(filtered_statuses);
return result;
}
async function test(github, owner, repo, sha) {
// Get statuses from sha
const statuses = await getCommitStatuses(github, owner, repo, sha);
return calculateCheckResult(statuses);
}
async function createCheck(github, owner, repo, sha) {
const check = await github.rest.checks.create({
owner,
repo,
head_sha: sha,
name: 'Build Outcome',
status: 'in_progress',
});
return check.data.id;
}
async function updateCheck(github, owner, repo, checkRunId, status, description) {
// To ensure that
const checkStatus = status == 'pending' ? 'in_progress' : 'completed';
const conclusion = checkStatus == 'in_progress' ? undefined : (status == 'success' ? 'success' : 'failure');
await github.rest.checks.update({
owner,
repo,
check_run_id: checkRunId,
status: checkStatus,
conclusion,
output: {
title: description,
summary: ''
}
});
}
// END OF CLONED SECTION
// THIS SECTION MUST BE INCLUDED, UNCOMMENTED IN THE .yml
// const { owner, repo } = context.repo;
// const sha = context.payload?.check_suite?.sha || context.sha;
// const checkRunId = await createCheck(github, owner, repo, sha);
// const res = await test(github, owner, repo, sha);
// await updateCheck(github, owner, repo, checkRunId, res[0], res[1]);
// END OF COMMENTED SECTION
// Following code is used only for unit testing; do not include in the .yml
export const unitTestEndpoints = {
getCommitStatuses, calculateCheckResult, getCommitCheckRuns
};

View file

@ -0,0 +1,44 @@
import * as fs from 'node:fs';
import 'mocha';
import { assert } from 'chai';
import { unitTestEndpoints } from './pr-build-status.mjs';
// debug only
import { getOctokit } from '@actions/github';
import * as process from 'node:process';
const commits = {
'825f7d2df0069fa56a8f2a534b42a49ee1f21ca1': [ 'success', '24 checks completed successfully ✅' ],
'870d37f56b489ef5067b4352a06505369d9038e0': [ 'error', '6 checks in an error state ❌; 17 checks completed successfully ✅' ],
'11a558e926370253db37026c95b7ff5a6aa1e8fc': [ 'success', '22 checks completed successfully ✅' ],
'8fd5ccc9026b85d6a780d8bcf862380e7a89aefb': [ 'pending', 'Checks have not yet been triggered ⌛' ],
'153683cfb007c6066c9dcf71d25afa4c66efa17f': [ 'pending', 'Checks have not yet been triggered ⌛' ],
};
// When adding new SHAs to test, run this to collect data from GitHub
const debug = false;
if(debug) {
async function writeTestFile(sha) {
fs.writeFileSync('fixtures/' + sha + '-statuses.json',
JSON.stringify(await unitTestEndpoints.getCommitStatuses(octokit, 'keymanapp', 'keyman', sha), null, 2), 'utf-8');
fs.writeFileSync('fixtures/' + sha + '-check-runs.json',
JSON.stringify(await unitTestEndpoints.getCommitCheckRuns(octokit, 'keymanapp', 'keyman', sha), null, 2), 'utf-8');
}
const octokit = getOctokit(process.env['GITHUB_TOKEN']);
for(const sha of Object.keys(commits)) await writeTestFile(sha);
}
describe('pr-build-status', function () {
for(const sha of Object.keys(commits)) {
it(`should return '${commits[sha][1]}' for sha ${sha}`, function () {
const json = JSON.parse(fs.readFileSync(`fixtures/${sha}-statuses.json`,'utf-8'));
const status = unitTestEndpoints.calculateCheckResult(json);
assert.isNotNull(status);
assert.isArray(status);
assert.deepStrictEqual(status, commits[sha]);
});
}
});

View file

@ -1,22 +1,51 @@
#!/usr/bin/env bash
#
# Determine if we need to do a build based on rules in
# trigger-definitions.inc.sh, rather than calculating changes in
# TeamCity. If a build is needed, then we ask TeamCity to
# start the build.
# trigger-definitions.inc.sh and based on commit trailers and PR body comments,
# rather than calculating changes in TeamCity. If a build is needed, then we ask
# TeamCity / GHA to start the build.
#
set -e
set -u
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
if [[ $# -lt 1 ]]; then
echo 'Usage: run-required-test-build.sh pull-request-number|branch'
echo ' where branch can be master, beta'
exit 1
. "${THIS_SCRIPT%/*}/trigger-definitions.inc.sh"
. "${THIS_SCRIPT%/*}/trigger-builds.inc.sh"
. "${THIS_SCRIPT%/*}/trigger-build-bot.inc.sh"
. "${THIS_SCRIPT%/*}/jq.inc.sh"
builder_describe "Run test builds for the given pull request/primary branch" \
"--dry-run,-n Only report back which builds would be started" \
"--branch,-b=PRNUM Branch (master,beta,stable-x.y) or pull-request to test"
if [[ $# -eq 1 ]] && [[ "$1" =~ ^([0-9]+|master|beta|stable-[0-9]+\.[0-9]+)$ ]]; then
# For transitional period, when build configuration on TC needs to support
# both forms, we will pass a modified set of params to builder_parse
#
# TODO: remove this condition once all branches have received this change,
# and update TC build configuration to pass -b parameter
#
builder_parse -b "$1"
else
PRNUM="$1"
builder_parse "$@"
fi
# Validate parameters
if ! builder_has_option --branch; then
builder_die "--branch parameter is required"
fi
if [[ ! "$PRNUM" =~ ^([0-9]+|master|beta|stable-[0-9]+\.[0-9]+)$ ]]; then
builder_die "Invalid parameter --branch '$PRNUM'; expected 'master', 'beta', 'stable-x.y', or PR number"
fi
#
# Debug logging
#
function debug_echo() {
if [ ! -z ${DEBUG:-} ]; then
echo "DEBUG: $1"
@ -25,48 +54,77 @@ function debug_echo() {
}
#
# This must be run under a TeamCity environment in order to receive all variables
#
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../resources/build/build-utils.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "${THIS_SCRIPT%/*}/trigger-definitions.inc.sh"
. "${THIS_SCRIPT%/*}/trigger-builds.inc.sh"
. "${THIS_SCRIPT%/*}/jq.inc.sh"
#
# Iterate through the platforms 'array' passed in and
# Iterate through the platforms array passed in and
# run builds associated with each platform found
#
# Parameters:
# 1: name of associative array of platforms, platform=skip|build|release|[fulltest?]
# 2: branch name or PR number
#
function triggerTestBuilds() {
# Note: we always run builds for 'all' platforms
local platforms=( `echo "all $1"` )
local -n platforms=$1
local branch="$2"
local force="${3:-false}"
local found_build=false
# Cancel any already running builds for this branch
node "$THIS_SCRIPT_PATH/ci/cancel-builds/cancel-test-builds.mjs" "$branch" "$TEAMCITY_TOKEN"
if builder_has_option --dry-run; then
builder_echo "DRY RUN: cancel current builds for $branch"
else
node "$THIS_SCRIPT_PATH/ci/cancel-builds/cancel-test-builds.mjs" "$branch" "$TEAMCITY_TOKEN"
fi
for platform in "${platforms[@]}"; do
echo "# $platform: checking for changes"
for platform in "${!platforms[@]}"; do
local platformBuildLevel=${platforms[$platform]}
if [[ $platformBuildLevel == skip ]]; then
builder_echo heading "$platform: skipping build"
continue
fi
builder_echo heading "$platform: checking for git changes"
eval test_builds='(${'bc_test_$platform'[@]})'
for test_build in "${test_builds[@]}"; do
if [[ $test_build == "" ]]; then continue; fi
found_build=true
if [ "${test_build:(-7)}" == "_GitHub" ]; then
local job=${test_build%_GitHub}
echo " -- Triggering GitHub action build $job/$branch"
triggerGitHubActionsBuild true "$job" "$branch"
if builder_has_option --dry-run; then
builder_echo "DRY RUN: Triggering GitHub action build $job/$branch, level = $platformBuildLevel"
else
builder_echo "Triggering GitHub action build $job/$branch, level = $platformBuildLevel"
triggerGitHubActionsBuild true $platformBuildLevel "$job" "$branch"
fi
else
echo " -- Triggering build configuration $test_build on teamcity"
triggerTeamCityBuild true "$test_build" "$vcs_test" "$branch"
if builder_has_option --dry-run; then
builder_echo "DRY RUN: Triggering build configuration $test_build on teamcity for $branch, level = $platformBuildLevel"
else
builder_echo "Triggering build configuration $test_build on teamcity for $branch, level = $platformBuildLevel"
triggerTeamCityBuild true $platformBuildLevel "$test_build" "$vcs_test" "$branch"
fi
fi
done
done
if [[ $found_build == false ]]; then
postSkippedBuildsStatusResult
fi
}
#
# Add a 'Test Build (Keyman)' successful status check to the commit
#
function postSkippedBuildsStatusResult() {
if builder_has_option --dry-run; then
builder_echo "DRY RUN: write successful status check 'Skipping since no platform builds necessary'"
else
curl --silent --write-out '\n' \
--request POST \
--header "Accept: application/vnd.github+json" \
--header "Authorization: token $GITHUB_TOKEN" \
--data '{"state":"success","description":"Skipping since no platform builds necessary","context":"Test Build (Keyman)"}' \
"https://api.github.com/repos/keymanapp/keyman/statuses/${BUILD_VCS_NUMBER}"
fi
}
#
@ -75,9 +133,9 @@ function triggerTestBuilds() {
#
if [[ ! "$PRNUM" =~ ^[[:digit:]]+$ ]]; then
# branch name is 'master', 'beta' [, or 'stable' -- in the future]
echo ". Branch $PRNUM needs to pass tests on all platforms."
triggerTestBuilds "`echo ${available_platforms[@]}`" "$PRNUM" "true"
# branch name is 'master', 'beta', or 'stable-x.y'
builder_echo "Branch $PRNUM needs to pass tests on all platforms."
triggerTestBuilds main_branch_platform_build_levels $PRNUM
exit 0
fi
@ -88,7 +146,7 @@ fi
# targeted by the PR other than to ask GitHub, anyway.)
#
echo ". Get information about pull request #$PRNUM from GitHub"
builder_echo grey "# Get information about pull request #$PRNUM from GitHub"
prinfo=`curl -s -H "User-Agent: @keymanapp" -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/keymanapp/keyman/pulls/$PRNUM`
prbase=`echo ${prinfo} | "$JQ" -r '.base.ref'`
prhead=`echo ${prinfo} | "$JQ" -r '.head.ref'`
@ -111,7 +169,7 @@ fi
#
# (Ensure we are within the repository before running git calls)
echo ". Ensure our local branch is up to date"
builder_echo grey "# Ensure our local branch is up to date"
pushd "$KEYMAN_ROOT" > /dev/null
git fetch origin > /dev/null
@ -132,7 +190,7 @@ fi
# We work from origin so we don't need the branches in our local copy
#
echo ". Get list of changed files in the pull request"
builder_echo grey "# Get list of changed files in the pull request"
prfiles=`git diff --name-only "origin/$prbase"..."$prremote/$prhead" || ( if [ $? == 128 ]; then echo abort; else exit $?; fi )`
if [ "$prfiles" == "abort" ]; then
# Don't trigger any builds, exit with success
@ -155,43 +213,58 @@ popd > /dev/null
# Find the platforms that have changes based on the watch_ variables in trigger-definitions.inc.sh
#
echo ". Find platforms that have changes"
build_platforms=()
function find_platform_changes() {
builder_echo grey "# Find platforms that have changes"
declare -gA build_platforms
local platform watch
# Scan the files found
while IFS= read -r line; do
# for each platform
for platform in "${available_platforms[@]}"; do
if [[ ! " ${build_platforms[@]} " =~ " $platform " ]]; then
# Which platform are we watching?
eval watch='$'watch_$platform
# Add common patterns to the watch list
watch="^(${platform}|(oem/[^/]+/${platform})|resources/((?!teamcity)|teamcity/(${platform}|includes))|${watch})"
# Since bash doesn't support negative look-aheads we use perl to test
# (grep has a --perl-regexp option, but not the version on macOS)
if perl -e 'exit($ARGV[0] =~ /$ARGV[1]/ ? 0 : 1)' "${line}" "${watch}"; then
build_platforms+=($platform)
# Scan the files found
while IFS= read -r line; do
# for each platform
for platform in "${available_platforms[@]}"; do
if [[ ! " ${!build_platforms[@]} " =~ " $platform " ]]; then
# Which platform are we watching?
eval watch='$'watch_$platform
# Add common patterns to the watch list
watch="^(${platform}|(oem/[^/]+/${platform})|resources/((?!teamcity)|teamcity/(${platform}|includes))|${watch})"
# Since bash doesn't support negative look-aheads we use perl to test
# (grep has a --perl-regexp option, but not the version on macOS)
if perl -e 'exit($ARGV[0] =~ /$ARGV[1]/ ? 0 : 1)' "${line}" "${watch}"; then
# By default, we'll build a 'release' test build for touched platforms
build_platforms[$platform]=$build_level_release
fi
fi
fi
done
done <<< "$prfiles"
done
done <<< "$prfiles"
debug_echo "Build platforms: ${build_platforms[*]}"
debug_echo "Default build platforms: ${!build_platforms[@]}"
}
find_platform_changes
#
# Now check PR commits for Build-bot: commands
# This will modify the build_platforms array
#
if [ "$prremote" == "origin" ]; then
# We only accept Build-bot commands on trusted local origin PRs
prcommits=`curl -s -H "User-Agent: @keymanapp" -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/keymanapp/keyman/pulls/$PRNUM/commits`
test_bot_check_pr_body "$PRNUM" "$prinfo"
build_bot_check_messages $PRNUM "$prinfo" "$prcommits"
fi
#
# Start test builds as required
#
if (( ${#build_platforms[@]} > 0)); then
#
# Start the test builds
#
echo ". Start test builds"
triggerTestBuilds "`echo ${build_platforms[@]}`" "$PRNUM"
builder_echo heading "Start test builds"
triggerTestBuilds build_platforms $PRNUM
else
echo ". No builds to start"
curl --silent --write-out '\n' \
--request POST \
--header "Accept: application/vnd.github+json" \
--header "Authorization: token $GITHUB_TOKEN" \
--data '{"state":"success","description":"Skipping since no platform builds necessary","context":"Test Build (Keyman)"}' \
"https://api.github.com/repos/keymanapp/keyman/statuses/${BUILD_VCS_NUMBER}"
builder_echo heading "No builds to start"
postSkippedBuildsStatusResult
fi
exit 0

View file

@ -0,0 +1,171 @@
[
{
"sha": "b7970d439cdd18e84fd754bf0f06a8878c18bb97",
"node_id": "C_kwDOAY2xT9oAKGI3OTcwZDQzOWNkZDE4ZTg0ZmQ3NTRiZjBmMDZhODg3OGMxOGJiOTc",
"commit": {
"author": {
"name": "Marc Durdin",
"email": "marc@durdin.net",
"date": "2025-04-30T01:59:27Z"
},
"committer": {
"name": "Marc Durdin",
"email": "marc@durdin.net",
"date": "2025-04-30T01:59:27Z"
},
"message": "maint(common): consolidate VC++ environment setup for meson\n\nUse the automated environment setup that we built for Windows and\nDeveloper builds in our meson build configurations, so we don't need to\ncall out to vsdevcmd.bat for every build step. This makes the build\nfaster, more consistent across the various ways we call VC++, and\nsimplifies the shell scripts. Removes the complicated build.bat wrappers.\n\nAlso removes the `cleanup_visual_studio_path` function which should no\nlonger be necessary, as our environment builder does the job for us.\n\nMinor complication in that `$TARGET_PATH` variable in kmcmplib/build.sh\nis also used in the Windows environment setup, so eliminated its use in\nkmcmplib/build.sh.\n\nFinally, removes the `$MESON_LOW_VERSION` variable as we know we are\nalways on Meson 1.0 or later now.\n\nBuild-bot: skip developer, windows, foo\nBuild-bot: bash windows",
"tree": {
"sha": "df937f0cb23e2e3736d92cf39b3b7f62ce20bcc1",
"url": "https://api.github.com/repos/keymanapp/keyman/git/trees/df937f0cb23e2e3736d92cf39b3b7f62ce20bcc1"
},
"url": "https://api.github.com/repos/keymanapp/keyman/git/commits/b7970d439cdd18e84fd754bf0f06a8878c18bb97",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null,
"verified_at": null
}
},
"url": "https://api.github.com/repos/keymanapp/keyman/commits/b7970d439cdd18e84fd754bf0f06a8878c18bb97",
"html_url": "https://github.com/keymanapp/keyman/commit/b7970d439cdd18e84fd754bf0f06a8878c18bb97",
"comments_url": "https://api.github.com/repos/keymanapp/keyman/commits/b7970d439cdd18e84fd754bf0f06a8878c18bb97/comments",
"author": {
"login": "mcdurdin",
"id": 4498365,
"node_id": "MDQ6VXNlcjQ0OTgzNjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/4498365?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/mcdurdin",
"html_url": "https://github.com/mcdurdin",
"followers_url": "https://api.github.com/users/mcdurdin/followers",
"following_url": "https://api.github.com/users/mcdurdin/following{/other_user}",
"gists_url": "https://api.github.com/users/mcdurdin/gists{/gist_id}",
"starred_url": "https://api.github.com/users/mcdurdin/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/mcdurdin/subscriptions",
"organizations_url": "https://api.github.com/users/mcdurdin/orgs",
"repos_url": "https://api.github.com/users/mcdurdin/repos",
"events_url": "https://api.github.com/users/mcdurdin/events{/privacy}",
"received_events_url": "https://api.github.com/users/mcdurdin/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"committer": {
"login": "mcdurdin",
"id": 4498365,
"node_id": "MDQ6VXNlcjQ0OTgzNjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/4498365?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/mcdurdin",
"html_url": "https://github.com/mcdurdin",
"followers_url": "https://api.github.com/users/mcdurdin/followers",
"following_url": "https://api.github.com/users/mcdurdin/following{/other_user}",
"gists_url": "https://api.github.com/users/mcdurdin/gists{/gist_id}",
"starred_url": "https://api.github.com/users/mcdurdin/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/mcdurdin/subscriptions",
"organizations_url": "https://api.github.com/users/mcdurdin/orgs",
"repos_url": "https://api.github.com/users/mcdurdin/repos",
"events_url": "https://api.github.com/users/mcdurdin/events{/privacy}",
"received_events_url": "https://api.github.com/users/mcdurdin/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"parents": [
{
"sha": "926be6e9463e0497dbf1d32d4ef908ae2aad20db",
"url": "https://api.github.com/repos/keymanapp/keyman/commits/926be6e9463e0497dbf1d32d4ef908ae2aad20db",
"html_url": "https://github.com/keymanapp/keyman/commit/926be6e9463e0497dbf1d32d4ef908ae2aad20db"
}
]
},
{
"sha": "0230010a509e317f835d18e1d3be116c30047cf9",
"node_id": "C_kwDOAY2xT9oAKDAyMzAwMTBhNTA5ZTMxN2Y4MzVkMThlMWQzYmUxMTZjMzAwNDdjZjk",
"commit": {
"author": {
"name": "Marc Durdin",
"email": "marc@durdin.net",
"date": "2025-04-30T02:18:26Z"
},
"committer": {
"name": "GitHub",
"email": "noreply@github.com",
"date": "2025-04-30T02:18:26Z"
},
"message": "Merge branch 'maint/meson-versioning-improvements' into maint/core-developer/12639-meson-no-batch-wrappers\n\nBuild-bot: skip all",
"tree": {
"sha": "47bb7ca6f52998e8a14e478f5e9f5d7ba415ace3",
"url": "https://api.github.com/repos/keymanapp/keyman/git/trees/47bb7ca6f52998e8a14e478f5e9f5d7ba415ace3"
},
"url": "https://api.github.com/repos/keymanapp/keyman/git/commits/0230010a509e317f835d18e1d3be116c30047cf9",
"comment_count": 0,
"verification": {
"verified": true,
"reason": "valid",
"signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJoEYhyCRC1aQ7uu5UhlAAAg4MQAFxQGq7WPhHNzlspVIFv5k8W\nJPmC2SP+eUXyDKpO38MqnAPlMqaNrleB071BtE9i2DeWHwYvGwJ87ki95aAtg00F\ne4RbhJYy5sW/F/0fNzHgGNFPNb3EcRyt6I7X3xRB+i1k0PgTSVIpnLzXn3gj9GwU\n7PuLgNO0CUpcgWc6X7a259RAlLInEJ+I8p9XhW827eRalogwgALt4IuJmWyJALFL\nJFeWGlCKdVfbHEgBgEyA98zNnvtoxQ+FlJn1V1jJEBpCY+MPB5tyCXZ4LZbn/n7V\n0+2Pp3c5lnZKmaOyUykwQw7P8s4R2+aQnghRHEHa5Fm6hjQkHMiCOwgTwpIrUV+U\n4AWzPjZSw71cBmAUJBUe7SIBv51r1TUeuNy9x3/0r588znBItR12C91KQD8u9o3/\nMqcWTkpEuGgD2nYbhTdb9PNF4Ik/wbBKLDvjQg180b/ocklVaGAAqjOHdvkyQ4in\nIBRqiaeI3zD5TQ7936Sk1JQPil97PWzk27vde0KAeCR59yL8lCnHr2Rtp8hEpytX\nGrlL14GIaVK6YEvumloPFXrWnbdETu0yRllVa4T2RbWskZUtPG93l/fmOV6tiAr2\np79lgJQ0eEJ1fBNm9UsvjDXU478fSU5Dobg1pwv+wxr10u43tPbqmMI6VLxOjt7i\nz44SZyX8udppGuASFSsw\n=3UIv\n-----END PGP SIGNATURE-----\n",
"payload": "tree 47bb7ca6f52998e8a14e478f5e9f5d7ba415ace3\nparent b7970d439cdd18e84fd754bf0f06a8878c18bb97\nparent e976b5563589a9e38c778f3c06e404e82526358a\nauthor Marc Durdin <marc@durdin.net> 1745979506 +0700\ncommitter GitHub <noreply@github.com> 1745979506 +0700\n\nMerge branch 'maint/meson-versioning-improvements' into maint/core-developer/12639-meson-no-batch-wrappers",
"verified_at": "2025-04-30T02:18:29Z"
}
},
"url": "https://api.github.com/repos/keymanapp/keyman/commits/0230010a509e317f835d18e1d3be116c30047cf9",
"html_url": "https://github.com/keymanapp/keyman/commit/0230010a509e317f835d18e1d3be116c30047cf9",
"comments_url": "https://api.github.com/repos/keymanapp/keyman/commits/0230010a509e317f835d18e1d3be116c30047cf9/comments",
"author": {
"login": "mcdurdin",
"id": 4498365,
"node_id": "MDQ6VXNlcjQ0OTgzNjU=",
"avatar_url": "https://avatars.githubusercontent.com/u/4498365?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/mcdurdin",
"html_url": "https://github.com/mcdurdin",
"followers_url": "https://api.github.com/users/mcdurdin/followers",
"following_url": "https://api.github.com/users/mcdurdin/following{/other_user}",
"gists_url": "https://api.github.com/users/mcdurdin/gists{/gist_id}",
"starred_url": "https://api.github.com/users/mcdurdin/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/mcdurdin/subscriptions",
"organizations_url": "https://api.github.com/users/mcdurdin/orgs",
"repos_url": "https://api.github.com/users/mcdurdin/repos",
"events_url": "https://api.github.com/users/mcdurdin/events{/privacy}",
"received_events_url": "https://api.github.com/users/mcdurdin/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"committer": {
"login": "web-flow",
"id": 19864447,
"node_id": "MDQ6VXNlcjE5ODY0NDQ3",
"avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/web-flow",
"html_url": "https://github.com/web-flow",
"followers_url": "https://api.github.com/users/web-flow/followers",
"following_url": "https://api.github.com/users/web-flow/following{/other_user}",
"gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}",
"starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/web-flow/subscriptions",
"organizations_url": "https://api.github.com/users/web-flow/orgs",
"repos_url": "https://api.github.com/users/web-flow/repos",
"events_url": "https://api.github.com/users/web-flow/events{/privacy}",
"received_events_url": "https://api.github.com/users/web-flow/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"parents": [
{
"sha": "b7970d439cdd18e84fd754bf0f06a8878c18bb97",
"url": "https://api.github.com/repos/keymanapp/keyman/commits/b7970d439cdd18e84fd754bf0f06a8878c18bb97",
"html_url": "https://github.com/keymanapp/keyman/commit/b7970d439cdd18e84fd754bf0f06a8878c18bb97"
},
{
"sha": "e976b5563589a9e38c778f3c06e404e82526358a",
"url": "https://api.github.com/repos/keymanapp/keyman/commits/e976b5563589a9e38c778f3c06e404e82526358a",
"html_url": "https://github.com/keymanapp/keyman/commit/e976b5563589a9e38c778f3c06e404e82526358a"
}
]
}
]

View file

@ -0,0 +1,534 @@
{
"url": "https://api.github.com/repos/keymanapp/keyman/pulls/14013",
"id": 2533896821,
"node_id": "PR_kwDOAY2xT86XCDJ1",
"html_url": "https://github.com/keymanapp/keyman/pull/14013",
"diff_url": "https://github.com/keymanapp/keyman/pull/14013.diff",
"patch_url": "https://github.com/keymanapp/keyman/pull/14013.patch",
"issue_url": "https://api.github.com/repos/keymanapp/keyman/issues/14013",
"number": 14013,
"state": "open",
"locked": false,
"title": "feat(developer): kmc-convert validate keylayout file 😎",
"user": {
"login": "SabineSIL",
"id": 86713187,
"node_id": "MDQ6VXNlcjg2NzEzMTg3",
"avatar_url": "https://avatars.githubusercontent.com/u/86713187?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/SabineSIL",
"html_url": "https://github.com/SabineSIL",
"followers_url": "https://api.github.com/users/SabineSIL/followers",
"following_url": "https://api.github.com/users/SabineSIL/following{/other_user}",
"gists_url": "https://api.github.com/users/SabineSIL/gists{/gist_id}",
"starred_url": "https://api.github.com/users/SabineSIL/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/SabineSIL/subscriptions",
"organizations_url": "https://api.github.com/users/SabineSIL/orgs",
"repos_url": "https://api.github.com/users/SabineSIL/repos",
"events_url": "https://api.github.com/users/SabineSIL/events{/privacy}",
"received_events_url": "https://api.github.com/users/SabineSIL/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"body": "\r\n\r\n**The setup:** \r\n- The xsd-file : `resources\\standards-data\\keylayout\\dtd\\keylayout.xsd`\r\n- The `resources\\standards-data\\keylayout\\create_keylayout_schema.sh` exists and creates `resources\\standards-data\\keylayout\\keylayout.schema.json`\r\n- The `common/web/types/build.sh configure` creates a `common\\web\\types\\src\\schemas\\keylayout.schema.validator.mjs` file \r\n- The .mjs is then used in validate() function of `keylayout-file-reader.ts` (`...SchemaValidators.default.keylayout(source)`) \r\n\r\n**The problem:** \r\nAll the above works but while using the `validate20()` function of `keylayout.schema.validator.mjs` , **key0 will be \"?xml\"** which then produces the error.\r\n \r\n> \r\n> for (const **key0** in data) {\r\n> if (!(key0 === \"keyboard\")) {\r\n> validate20.errors = [{ instancePath, schemaPath: \"#/additionalProperties\", keyword: \"additionalProperties\", params: { additionalProperty: key0 }, message: \"must NOT have additional properties\" }];\r\n> return false;\r\n> break;\r\n> }\r\n> }\r\n\r\n**The error:** \r\n![image](https://github.com/user-attachments/assets/efa4a0ba-a53a-419f-9d39-15c61ce26fe5)\r\n\r\n**So the question is: Why is `?xml `treated as an element tag and how can we prevent this**\r\n\r\n@keymanapp-test-bot skip\r\n\r\nBuild-bot: skip",
"created_at": "2025-05-21T10:02:29Z",
"updated_at": "2025-05-26T01:30:35Z",
"closed_at": null,
"merged_at": null,
"merge_commit_sha": null,
"assignee": {
"login": "SabineSIL",
"id": 86713187,
"node_id": "MDQ6VXNlcjg2NzEzMTg3",
"avatar_url": "https://avatars.githubusercontent.com/u/86713187?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/SabineSIL",
"html_url": "https://github.com/SabineSIL",
"followers_url": "https://api.github.com/users/SabineSIL/followers",
"following_url": "https://api.github.com/users/SabineSIL/following{/other_user}",
"gists_url": "https://api.github.com/users/SabineSIL/gists{/gist_id}",
"starred_url": "https://api.github.com/users/SabineSIL/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/SabineSIL/subscriptions",
"organizations_url": "https://api.github.com/users/SabineSIL/orgs",
"repos_url": "https://api.github.com/users/SabineSIL/repos",
"events_url": "https://api.github.com/users/SabineSIL/events{/privacy}",
"received_events_url": "https://api.github.com/users/SabineSIL/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"assignees": [
{
"login": "SabineSIL",
"id": 86713187,
"node_id": "MDQ6VXNlcjg2NzEzMTg3",
"avatar_url": "https://avatars.githubusercontent.com/u/86713187?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/SabineSIL",
"html_url": "https://github.com/SabineSIL",
"followers_url": "https://api.github.com/users/SabineSIL/followers",
"following_url": "https://api.github.com/users/SabineSIL/following{/other_user}",
"gists_url": "https://api.github.com/users/SabineSIL/gists{/gist_id}",
"starred_url": "https://api.github.com/users/SabineSIL/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/SabineSIL/subscriptions",
"organizations_url": "https://api.github.com/users/SabineSIL/orgs",
"repos_url": "https://api.github.com/users/SabineSIL/repos",
"events_url": "https://api.github.com/users/SabineSIL/events{/privacy}",
"received_events_url": "https://api.github.com/users/SabineSIL/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
}
],
"requested_reviewers": [
],
"requested_teams": [
],
"labels": [
{
"id": 648605999,
"node_id": "MDU6TGFiZWw2NDg2MDU5OTk=",
"url": "https://api.github.com/repos/keymanapp/keyman/labels/developer/",
"name": "developer/",
"color": "f9d0c4",
"default": false,
"description": ""
},
{
"id": 1095223705,
"node_id": "MDU6TGFiZWwxMDk1MjIzNzA1",
"url": "https://api.github.com/repos/keymanapp/keyman/labels/common/",
"name": "common/",
"color": "006b75",
"default": false,
"description": ""
},
{
"id": 1298035273,
"node_id": "MDU6TGFiZWwxMjk4MDM1Mjcz",
"url": "https://api.github.com/repos/keymanapp/keyman/labels/developer/compilers/",
"name": "developer/compilers/",
"color": "f9d0c4",
"default": false,
"description": ""
},
{
"id": 1608627704,
"node_id": "MDU6TGFiZWwxNjA4NjI3NzA0",
"url": "https://api.github.com/repos/keymanapp/keyman/labels/common/resources/",
"name": "common/resources/",
"color": "006b75",
"default": false,
"description": "Build infrastructure"
},
{
"id": 2058996244,
"node_id": "MDU6TGFiZWwyMDU4OTk2MjQ0",
"url": "https://api.github.com/repos/keymanapp/keyman/labels/feat",
"name": "feat",
"color": "84b6eb",
"default": false,
"description": ""
},
{
"id": 2073602939,
"node_id": "MDU6TGFiZWwyMDczNjAyOTM5",
"url": "https://api.github.com/repos/keymanapp/keyman/labels/common/web/",
"name": "common/web/",
"color": "006b75",
"default": false,
"description": ""
},
{
"id": 7333596907,
"node_id": "LA_kwDOAY2xT88AAAABtR3O6w",
"url": "https://api.github.com/repos/keymanapp/keyman/labels/epic-kmc-convert",
"name": "epic-kmc-convert",
"color": "F9C2EE",
"default": false,
"description": ""
}
],
"milestone": {
"url": "https://api.github.com/repos/keymanapp/keyman/milestones/229",
"html_url": "https://github.com/keymanapp/keyman/milestone/229",
"labels_url": "https://api.github.com/repos/keymanapp/keyman/milestones/229/labels",
"id": 12655812,
"node_id": "MI_kwDOAY2xT84AwRzE",
"number": 229,
"title": "A19S4",
"description": null,
"creator": {
"login": "keyman-server",
"id": 7018967,
"node_id": "MDQ6VXNlcjcwMTg5Njc=",
"avatar_url": "https://avatars.githubusercontent.com/u/7018967?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/keyman-server",
"html_url": "https://github.com/keyman-server",
"followers_url": "https://api.github.com/users/keyman-server/followers",
"following_url": "https://api.github.com/users/keyman-server/following{/other_user}",
"gists_url": "https://api.github.com/users/keyman-server/gists{/gist_id}",
"starred_url": "https://api.github.com/users/keyman-server/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/keyman-server/subscriptions",
"organizations_url": "https://api.github.com/users/keyman-server/orgs",
"repos_url": "https://api.github.com/users/keyman-server/repos",
"events_url": "https://api.github.com/users/keyman-server/events{/privacy}",
"received_events_url": "https://api.github.com/users/keyman-server/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"open_issues": 108,
"closed_issues": 34,
"state": "open",
"created_at": "2025-04-02T03:26:05Z",
"updated_at": "2025-05-31T00:48:44Z",
"due_on": "2025-06-06T07:00:00Z",
"closed_at": null
},
"draft": true,
"commits_url": "https://api.github.com/repos/keymanapp/keyman/pulls/14013/commits",
"review_comments_url": "https://api.github.com/repos/keymanapp/keyman/pulls/14013/comments",
"review_comment_url": "https://api.github.com/repos/keymanapp/keyman/pulls/comments{/number}",
"comments_url": "https://api.github.com/repos/keymanapp/keyman/issues/14013/comments",
"statuses_url": "https://api.github.com/repos/keymanapp/keyman/statuses/00158fdeeefc98e7d2b507729e91fab845415f6f",
"head": {
"label": "keymanapp:feat/developer/kmc-convert-validateKeylayout",
"ref": "feat/developer/kmc-convert-validateKeylayout",
"sha": "00158fdeeefc98e7d2b507729e91fab845415f6f",
"user": {
"login": "keymanapp",
"id": 12402926,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjEyNDAyOTI2",
"avatar_url": "https://avatars.githubusercontent.com/u/12402926?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/keymanapp",
"html_url": "https://github.com/keymanapp",
"followers_url": "https://api.github.com/users/keymanapp/followers",
"following_url": "https://api.github.com/users/keymanapp/following{/other_user}",
"gists_url": "https://api.github.com/users/keymanapp/gists{/gist_id}",
"starred_url": "https://api.github.com/users/keymanapp/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/keymanapp/subscriptions",
"organizations_url": "https://api.github.com/users/keymanapp/orgs",
"repos_url": "https://api.github.com/users/keymanapp/repos",
"events_url": "https://api.github.com/users/keymanapp/events{/privacy}",
"received_events_url": "https://api.github.com/users/keymanapp/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"repo": {
"id": 26063183,
"node_id": "MDEwOlJlcG9zaXRvcnkyNjA2MzE4Mw==",
"name": "keyman",
"full_name": "keymanapp/keyman",
"private": false,
"owner": {
"login": "keymanapp",
"id": 12402926,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjEyNDAyOTI2",
"avatar_url": "https://avatars.githubusercontent.com/u/12402926?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/keymanapp",
"html_url": "https://github.com/keymanapp",
"followers_url": "https://api.github.com/users/keymanapp/followers",
"following_url": "https://api.github.com/users/keymanapp/following{/other_user}",
"gists_url": "https://api.github.com/users/keymanapp/gists{/gist_id}",
"starred_url": "https://api.github.com/users/keymanapp/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/keymanapp/subscriptions",
"organizations_url": "https://api.github.com/users/keymanapp/orgs",
"repos_url": "https://api.github.com/users/keymanapp/repos",
"events_url": "https://api.github.com/users/keymanapp/events{/privacy}",
"received_events_url": "https://api.github.com/users/keymanapp/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"html_url": "https://github.com/keymanapp/keyman",
"description": "Keyman cross platform input methods system running on Android, iOS, Linux, macOS, Windows and mobile and desktop web",
"fork": false,
"url": "https://api.github.com/repos/keymanapp/keyman",
"forks_url": "https://api.github.com/repos/keymanapp/keyman/forks",
"keys_url": "https://api.github.com/repos/keymanapp/keyman/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/keymanapp/keyman/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/keymanapp/keyman/teams",
"hooks_url": "https://api.github.com/repos/keymanapp/keyman/hooks",
"issue_events_url": "https://api.github.com/repos/keymanapp/keyman/issues/events{/number}",
"events_url": "https://api.github.com/repos/keymanapp/keyman/events",
"assignees_url": "https://api.github.com/repos/keymanapp/keyman/assignees{/user}",
"branches_url": "https://api.github.com/repos/keymanapp/keyman/branches{/branch}",
"tags_url": "https://api.github.com/repos/keymanapp/keyman/tags",
"blobs_url": "https://api.github.com/repos/keymanapp/keyman/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/keymanapp/keyman/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/keymanapp/keyman/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/keymanapp/keyman/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/keymanapp/keyman/statuses/{sha}",
"languages_url": "https://api.github.com/repos/keymanapp/keyman/languages",
"stargazers_url": "https://api.github.com/repos/keymanapp/keyman/stargazers",
"contributors_url": "https://api.github.com/repos/keymanapp/keyman/contributors",
"subscribers_url": "https://api.github.com/repos/keymanapp/keyman/subscribers",
"subscription_url": "https://api.github.com/repos/keymanapp/keyman/subscription",
"commits_url": "https://api.github.com/repos/keymanapp/keyman/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/keymanapp/keyman/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/keymanapp/keyman/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/keymanapp/keyman/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/keymanapp/keyman/contents/{+path}",
"compare_url": "https://api.github.com/repos/keymanapp/keyman/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/keymanapp/keyman/merges",
"archive_url": "https://api.github.com/repos/keymanapp/keyman/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/keymanapp/keyman/downloads",
"issues_url": "https://api.github.com/repos/keymanapp/keyman/issues{/number}",
"pulls_url": "https://api.github.com/repos/keymanapp/keyman/pulls{/number}",
"milestones_url": "https://api.github.com/repos/keymanapp/keyman/milestones{/number}",
"notifications_url": "https://api.github.com/repos/keymanapp/keyman/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/keymanapp/keyman/labels{/name}",
"releases_url": "https://api.github.com/repos/keymanapp/keyman/releases{/id}",
"deployments_url": "https://api.github.com/repos/keymanapp/keyman/deployments",
"created_at": "2014-11-01T21:01:44Z",
"updated_at": "2025-05-31T18:02:06Z",
"pushed_at": "2025-05-31T18:02:03Z",
"git_url": "git://github.com/keymanapp/keyman.git",
"ssh_url": "git@github.com:keymanapp/keyman.git",
"clone_url": "https://github.com/keymanapp/keyman.git",
"svn_url": "https://github.com/keymanapp/keyman",
"homepage": "https://keyman.com/",
"size": 489713,
"stargazers_count": 456,
"watchers_count": 456,
"language": "Pascal",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"has_discussions": false,
"forks_count": 120,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 1043,
"license": {
"key": "other",
"name": "Other",
"spdx_id": "NOASSERTION",
"url": null,
"node_id": "MDc6TGljZW5zZTA="
},
"allow_forking": true,
"is_template": false,
"web_commit_signoff_required": false,
"topics": [
"android",
"css",
"hacktoberfest",
"ios",
"javascript",
"keyboard",
"keyboard-layouts",
"keyman",
"linux",
"macos",
"unicode",
"web",
"windows"
],
"visibility": "public",
"forks": 120,
"open_issues": 1043,
"watchers": 456,
"default_branch": "master"
}
},
"base": {
"label": "keymanapp:feat/developer/kmc-convert",
"ref": "feat/developer/kmc-convert",
"sha": "c7f6d15105920d8df18d8973b4f24846fa14ea27",
"user": {
"login": "keymanapp",
"id": 12402926,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjEyNDAyOTI2",
"avatar_url": "https://avatars.githubusercontent.com/u/12402926?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/keymanapp",
"html_url": "https://github.com/keymanapp",
"followers_url": "https://api.github.com/users/keymanapp/followers",
"following_url": "https://api.github.com/users/keymanapp/following{/other_user}",
"gists_url": "https://api.github.com/users/keymanapp/gists{/gist_id}",
"starred_url": "https://api.github.com/users/keymanapp/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/keymanapp/subscriptions",
"organizations_url": "https://api.github.com/users/keymanapp/orgs",
"repos_url": "https://api.github.com/users/keymanapp/repos",
"events_url": "https://api.github.com/users/keymanapp/events{/privacy}",
"received_events_url": "https://api.github.com/users/keymanapp/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"repo": {
"id": 26063183,
"node_id": "MDEwOlJlcG9zaXRvcnkyNjA2MzE4Mw==",
"name": "keyman",
"full_name": "keymanapp/keyman",
"private": false,
"owner": {
"login": "keymanapp",
"id": 12402926,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjEyNDAyOTI2",
"avatar_url": "https://avatars.githubusercontent.com/u/12402926?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/keymanapp",
"html_url": "https://github.com/keymanapp",
"followers_url": "https://api.github.com/users/keymanapp/followers",
"following_url": "https://api.github.com/users/keymanapp/following{/other_user}",
"gists_url": "https://api.github.com/users/keymanapp/gists{/gist_id}",
"starred_url": "https://api.github.com/users/keymanapp/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/keymanapp/subscriptions",
"organizations_url": "https://api.github.com/users/keymanapp/orgs",
"repos_url": "https://api.github.com/users/keymanapp/repos",
"events_url": "https://api.github.com/users/keymanapp/events{/privacy}",
"received_events_url": "https://api.github.com/users/keymanapp/received_events",
"type": "Organization",
"user_view_type": "public",
"site_admin": false
},
"html_url": "https://github.com/keymanapp/keyman",
"description": "Keyman cross platform input methods system running on Android, iOS, Linux, macOS, Windows and mobile and desktop web",
"fork": false,
"url": "https://api.github.com/repos/keymanapp/keyman",
"forks_url": "https://api.github.com/repos/keymanapp/keyman/forks",
"keys_url": "https://api.github.com/repos/keymanapp/keyman/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/keymanapp/keyman/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/keymanapp/keyman/teams",
"hooks_url": "https://api.github.com/repos/keymanapp/keyman/hooks",
"issue_events_url": "https://api.github.com/repos/keymanapp/keyman/issues/events{/number}",
"events_url": "https://api.github.com/repos/keymanapp/keyman/events",
"assignees_url": "https://api.github.com/repos/keymanapp/keyman/assignees{/user}",
"branches_url": "https://api.github.com/repos/keymanapp/keyman/branches{/branch}",
"tags_url": "https://api.github.com/repos/keymanapp/keyman/tags",
"blobs_url": "https://api.github.com/repos/keymanapp/keyman/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/keymanapp/keyman/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/keymanapp/keyman/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/keymanapp/keyman/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/keymanapp/keyman/statuses/{sha}",
"languages_url": "https://api.github.com/repos/keymanapp/keyman/languages",
"stargazers_url": "https://api.github.com/repos/keymanapp/keyman/stargazers",
"contributors_url": "https://api.github.com/repos/keymanapp/keyman/contributors",
"subscribers_url": "https://api.github.com/repos/keymanapp/keyman/subscribers",
"subscription_url": "https://api.github.com/repos/keymanapp/keyman/subscription",
"commits_url": "https://api.github.com/repos/keymanapp/keyman/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/keymanapp/keyman/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/keymanapp/keyman/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/keymanapp/keyman/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/keymanapp/keyman/contents/{+path}",
"compare_url": "https://api.github.com/repos/keymanapp/keyman/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/keymanapp/keyman/merges",
"archive_url": "https://api.github.com/repos/keymanapp/keyman/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/keymanapp/keyman/downloads",
"issues_url": "https://api.github.com/repos/keymanapp/keyman/issues{/number}",
"pulls_url": "https://api.github.com/repos/keymanapp/keyman/pulls{/number}",
"milestones_url": "https://api.github.com/repos/keymanapp/keyman/milestones{/number}",
"notifications_url": "https://api.github.com/repos/keymanapp/keyman/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/keymanapp/keyman/labels{/name}",
"releases_url": "https://api.github.com/repos/keymanapp/keyman/releases{/id}",
"deployments_url": "https://api.github.com/repos/keymanapp/keyman/deployments",
"created_at": "2014-11-01T21:01:44Z",
"updated_at": "2025-05-31T18:02:06Z",
"pushed_at": "2025-05-31T18:02:03Z",
"git_url": "git://github.com/keymanapp/keyman.git",
"ssh_url": "git@github.com:keymanapp/keyman.git",
"clone_url": "https://github.com/keymanapp/keyman.git",
"svn_url": "https://github.com/keymanapp/keyman",
"homepage": "https://keyman.com/",
"size": 489713,
"stargazers_count": 456,
"watchers_count": 456,
"language": "Pascal",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"has_discussions": false,
"forks_count": 120,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 1043,
"license": {
"key": "other",
"name": "Other",
"spdx_id": "NOASSERTION",
"url": null,
"node_id": "MDc6TGljZW5zZTA="
},
"allow_forking": true,
"is_template": false,
"web_commit_signoff_required": false,
"topics": [
"android",
"css",
"hacktoberfest",
"ios",
"javascript",
"keyboard",
"keyboard-layouts",
"keyman",
"linux",
"macos",
"unicode",
"web",
"windows"
],
"visibility": "public",
"forks": 120,
"open_issues": 1043,
"watchers": 456,
"default_branch": "master"
}
},
"_links": {
"self": {
"href": "https://api.github.com/repos/keymanapp/keyman/pulls/14013"
},
"html": {
"href": "https://github.com/keymanapp/keyman/pull/14013"
},
"issue": {
"href": "https://api.github.com/repos/keymanapp/keyman/issues/14013"
},
"comments": {
"href": "https://api.github.com/repos/keymanapp/keyman/issues/14013/comments"
},
"review_comments": {
"href": "https://api.github.com/repos/keymanapp/keyman/pulls/14013/comments"
},
"review_comment": {
"href": "https://api.github.com/repos/keymanapp/keyman/pulls/comments{/number}"
},
"commits": {
"href": "https://api.github.com/repos/keymanapp/keyman/pulls/14013/commits"
},
"statuses": {
"href": "https://api.github.com/repos/keymanapp/keyman/statuses/00158fdeeefc98e7d2b507729e91fab845415f6f"
}
},
"author_association": "CONTRIBUTOR",
"auto_merge": null,
"active_lock_reason": null,
"merged": false,
"mergeable": false,
"rebaseable": false,
"mergeable_state": "dirty",
"merged_by": null,
"comments": 1,
"review_comments": 0,
"maintainer_can_modify": false,
"commits": 5,
"additions": 639,
"deletions": 12,
"changed_files": 13
}

View file

@ -0,0 +1,17 @@
[
{
"commit": {
"message": "maint(common): test\nBuild-bot: build windows\n"
}
},
{
"commit": {
"message": "maint(common): test\nBuild-bot: release windows\n"
}
},
{
"commit": {
"message": "maint(common): test\nBuild-bot: skip windows\n"
}
}
]

View file

@ -0,0 +1,3 @@
{
"body": "Build-bot: release developer\r\nBuild-bot: skip windows\r\n"
}

View file

@ -0,0 +1,202 @@
#!/usr/bin/env bash
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../../resources/build/build-utils.sh"
# END STANDARD BUILD SCRIPT INCLUDE
. "${THIS_SCRIPT_PATH}/../../trigger-definitions.inc.sh"
. "${THIS_SCRIPT_PATH}/../../trigger-build-bot.inc.sh"
. "${THIS_SCRIPT_PATH}/../test-utils.inc.sh"
. "${KEYMAN_ROOT}/resources/build/jq.inc.sh"
# Initialize builder, no input parameters needed for these tests
builder_parse ""
readonly ALL_BUILD_PLATFORMS_SKIP_EXPECTED='[ios]="skip" [web]="skip" [linux]="skip" [common_mac]="skip" [common_windows]="skip" [mac]="skip" [windows]="skip" [common_web]="skip" [android]="skip" [developer]="skip" [common_linux]="skip"'
readonly ALL_BUILD_PLATFORMS_BUILD_EXPECTED='[ios]="build" [web]="build" [linux]="build" [common_mac]="build" [common_windows]="build" [mac]="build" [windows]="build" [common_web]="build" [android]="build" [developer]="build" [common_linux]="build"'
#----------------------------------------------------------------------------------------------------
# Test build bot message parsing (e2e)
#----------------------------------------------------------------------------------------------------
test_build_bot_check_messages() {
builder_echo start test_build_bot_check_messages 'START TEST: build_bot_check_messages'
# Mostly real data (only Build-bot commands edited for test)
_do_test_build_bot_check_messages_file 14013 "[windows]=release" "$ALL_BUILD_PLATFORMS_SKIP_EXPECTED"
# Simplified data for testing Build-bot command sequences only
_do_test_build_bot_check_messages_file 9999 "[windows]=release" '[windows]="skip" [developer]="release"'
# test another sequence of commands
_do_test_build_bot_check_messages_inline 8 "[windows]=release" '[common]="release" [windows]="skip" [developer]="build"' '{ "body": "Build-bot: skip windows\nBuild-bot: build developer\nBuild-bot: release common" }' '[]'
# Test some bad inputs
_do_test_build_bot_check_messages_inline 1 "[windows]=release" '[windows]="release"' '{}' '[{ "commit": { "message": "maint(common): test\nBuild-bot: foo windows\n" }}]'
# empty body, 'foo windows' in commit
_do_test_build_bot_check_messages_inline 2 "[windows]=release" '[windows]="release"' '{ "body": "" }' '[{ "commit": { "message": "maint(common): test\nBuild-bot: foo windows\n" }}]'
# attempts to escape jail
_do_test_build_bot_check_messages_inline 3 "[windows]=release" '[windows]="release"' '{ "body": "Build-bot: '"'"'echo foo" }' '[{ "commit": { "message": "maint(common): test\n" }}]'
_do_test_build_bot_check_messages_inline 4 "[windows]=release" '[windows]="release"' '{ "body": "Build-bot: echo *" }' '[{ "commit": { "message": "maint(common): test\n" }}]'
_do_test_build_bot_check_messages_inline 5 "[windows]=release" '[windows]="release"' '{ "body": "Build-bot: \\0" }' '[{ "commit": { "message": "maint(common): test\n" }}]'
_do_test_build_bot_check_messages_inline 5 "[windows]=release" '[windows]="release"' '{ "body": "Build-bot: `echo escaped`" }' '[{ "commit": { "message": "maint(common): test\n" }}]'
_do_test_build_bot_check_messages_inline 6 "[windows]=release" '[windows]="release"' '{ "body": "Build-bot: ' '[{ "commit": { "message": "maint(common): test\n" }}]'
# incomplete command
_do_test_build_bot_check_messages_inline 7 "[windows]=release" '[windows]="release"' '{ "body": "Build-bot: " }' '[{ "commit": { "message": "maint(common): test\n" }}]'
builder_echo end test_build_bot_check_messages success 'SUCCESS: build_bot_check_messages'
}
#
# Test a set of Build-bot commands from a file
#
# Parameters:
# 1: PR number
# 2: Input build platforms based on touched files in PR
# 3: Expected build command
_do_test_build_bot_check_messages_file() {
local prnum=$1
eval "declare -gA build_platforms=($2)"
eval "declare -A expected_build_platforms=($3)"
build_bot_check_messages $prnum "$(cat ${THIS_SCRIPT_PATH}/pr-$prnum-data.txt)" "$(cat ${THIS_SCRIPT_PATH}/pr-$prnum-commits.txt)"
for i in "${!expected_build_platforms[@]}"; do
assert-equal "${build_platforms[$i]}" "${expected_build_platforms[$i]}" "PR #$prnum: build_platforms[$i]"
done
for i in "${!build_platforms[@]}"; do
assert-equal "${build_platforms[$i]}" "${expected_build_platforms[$i]}" "PR #$prnum: build_platforms[$i]"
done
}
#
# Test a set of Build-bot commands from parameters
#
# Parameters:
# 1: PR number
# 2: Input build platforms based on touched files in PR
# 3: Expected build command
# 4: JSON PR details (GitHub PR format)
# 5: JSON commit messages (GitHub PR format)
_do_test_build_bot_check_messages_inline() {
local prnum=$1
eval "declare -gA build_platforms=($2)"
eval "declare -A expected_build_platforms=($3)"
local prinfo="$4"
local prcommits="$5"
build_bot_check_messages $prnum "$prinfo" "$prcommits"
for i in "${!expected_build_platforms[@]}"; do
assert-equal "${build_platforms[$i]}" "${expected_build_platforms[$i]}" "PR #$prnum: build_platforms[$i]"
done
for i in "${!build_platforms[@]}"; do
assert-equal "${build_platforms[$i]}" "${expected_build_platforms[$i]}" "PR #$prnum: build_platforms[$i]"
done
}
#----------------------------------------------------------------------------------------------------
# Test parsing of a single Build-bot command
#----------------------------------------------------------------------------------------------------
test_build_bot_update_commands() {
builder_echo start test_build_bot_update_commands 'START TEST: build_bot_update_commands'
_do_test_build_bot_update_commands '[windows]="release"' "skip windows" '[windows]="skip"'
_do_test_build_bot_update_commands '[windows]="release"' "skip windows,developer" '[windows]="skip" [developer]="skip"'
_do_test_build_bot_update_commands '[windows]="release"' "build developer" '[windows]="release" [developer]="build"'
_do_test_build_bot_update_commands '[windows]="release"' "skip windows" '[windows]="skip"'
_do_test_build_bot_update_commands '[windows]="release"' "access foo" '[windows]="release"'
_do_test_build_bot_update_commands '[windows]="release"' "build foo" '[windows]="release"'
_do_test_build_bot_update_commands '[windows]="release"' "build common" '[common]="build" [windows]="release"'
_do_test_build_bot_update_commands '[windows]="release"' "build all" "$ALL_BUILD_PLATFORMS_BUILD_EXPECTED"
builder_echo end test_build_bot_update_commands success 'SUCCESS: build_bot_update_commands'
}
#
# Test a command
#
# Parameters:
# 1: starting build array based on PR files changed
# 2: build-bot command
# 3: expected finishing build array
#
_do_test_build_bot_update_commands() {
eval "declare -gA build_platforms=($1)"
local update_command="$2"
eval "declare -A expected_build_platforms=($3)"
build_bot_update_commands $update_command
for i in "${!expected_build_platforms[@]}"; do
assert-equal "${build_platforms[$i]}" "${expected_build_platforms[$i]}" "build_platforms[$i]"
done
for i in "${!build_platforms[@]}"; do
assert-equal "${build_platforms[$i]}" "${expected_build_platforms[$i]}" "build_platforms[$i]"
done
}
#----------------------------------------------------------------------------------------------------
test_build_bot_verify_platforms() {
builder_echo start test_build_bot_verify_platforms 'START TEST: build_bot_verify_platforms'
_do_test_build_bot_verify_platforms "windows mac" "windows mac"
_do_test_build_bot_verify_platforms "windows foo" "windows"
_do_test_build_bot_verify_platforms "core" "core"
_do_test_build_bot_verify_platforms "all" "${available_platforms[*]}"
builder_echo end test_build_bot_verify_platforms success 'SUCCESS: build_bot_verify_platforms'
}
_do_test_build_bot_verify_platforms() {
local platforms=($1)
local expected_platforms=($2)
build_bot_verify_platforms platforms
assert-equal ${#platforms[@]} ${#expected_platforms[@]} "#platforms[@]"
for i in "${!expected_platforms[@]}"; do
assert-equal "${platforms[$i]}" "${expected_platforms[$i]}" "platforms[$i]"
done
}
#----------------------------------------------------------------------------------------------------
# If we have a Test-bot: skip, or no Test-bot: command, then we downgrade
# 'release' to a 'build'
test_test_bot_check_pr_body() {
builder_echo start test_test_bot_check_pr_body 'START TEST: test_bot_check_pr_body'
# Test-bot: skip means that we should only do a build+test, not a release build
_do_test_test_bot_check_pr_body 1 '[windows]="release"' '{ "body": "Test-bot: skip" }' '[windows]="build"'
# Empty body means that we should only do a build+test, not a release build
_do_test_test_bot_check_pr_body 2 '[windows]="release"' '{ "body": "" }' '[windows]="build"'
# '# User Testing' we will assume means there are unit tests
_do_test_test_bot_check_pr_body 3 '[windows]="release"' '{ "body": "Some text\n# User Testing\n" }' '[windows]="release"'
# Invalid test-bot command, treat as a skip
_do_test_test_bot_check_pr_body 4 '[windows]="release"' '{ "body": "Test-bot: " }' '[windows]="build"'
builder_echo end test_test_bot_check_pr_body success 'SUCCESS: test_bot_check_pr_body'
}
_do_test_test_bot_check_pr_body() {
local prnum=$1
eval "declare -gA build_platforms=($2)"
local prinfo="$3"
eval "declare -A expected_build_platforms=($4)"
test_bot_check_pr_body $prnum "$prinfo"
for i in "${!expected_build_platforms[@]}"; do
assert-equal "${build_platforms[$i]}" "${expected_build_platforms[$i]}" "PR #$prnum: build_platforms[$i]"
done
for i in "${!build_platforms[@]}"; do
assert-equal "${build_platforms[$i]}" "${expected_build_platforms[$i]}" "PR #$prnum: build_platforms[$i]"
done
}
#----------------------------------------------------------------------------------------------------
test_build_bot_verify_platforms
test_build_bot_update_commands
test_build_bot_check_messages
test_test_bot_check_pr_body

View file

@ -7,8 +7,8 @@ function assert-equal() {
fi
if [[ "$actual" != "$expected" ]]; then
builder_die "FAIL: ${message}expected actual '$actual' to equal expected '$expected'"
builder_die " ✕ FAIL: ${message}actual result '$actual' should equal expected '$expected'"
else
builder_echo green "PASS: ${message}expected actual '$actual' to be equal to expected '$expected'"
builder_echo green " ✓ PASS: ${message}result '$actual' is correct"
fi
}

View file

@ -0,0 +1,164 @@
#!/usr/bin/env bash
# set -eu
# . ./trigger-definitions.inc.sh
#
# Check the Test-bot command in the PR body for commands relevant to the build
# bot, and update global build_platforms accordingly
#
# If the Test-bot command is 'skip', or there are no test bot instructions, then we
# will default to a 'build' for all default build platforms, instead of
# 'release'; this can be overridden by any Build-bot commands in commits or in
# the PR body.
#
# Parameters:
# 1: PR number (not currently used)
# 2: PR JSON data from api.github.com/repos/keymanapp/keyman/pulls/#
#
function test_bot_check_pr_body() {
local PRNUM=$1
local prinfo="$2"
set -o noglob
IFS=$'\n'
local prbody="$(echo "$prinfo" | "${JQ}" -r '.body')"
local prTestCommand="$(echo "$prbody" | grep 'Test-bot:' | cut -d: -f 2 - | cut -d' ' -f 1 -)"
local prTestBody="$(echo "$prbody" | grep -i '# User Testing')"
unset IFS
set +o noglob
if ([[ "$prTestCommand" == skip ]] || [[ -z "${prTestCommand// }" ]]) && [[ -z "${prTestBody// }" ]]; then
local platform
for platform in "${!build_platforms[@]}"; do
build_platforms[$platform]=build
done
fi
}
#
# Check PR commit messages for Build-bot commands. Later commands override
# earlier ones. Also checks PR body for any overriding commands. Note, only
# checks the first 250 commits, so later build commands will be ignored; in this
# situation, use the PR body command. Also ignores build commands in PR
# comments, for sake of performance
#
# The bot will also check the Test-bot: trailer in the PR body, if it exists, but
# will not check any other commits. If the Test-bot: command is just 'skip', then
# by default, all builds will be set to 'build' instead of 'release'
#
# Parameters:
# 1: PR number to check
# 2: PR JSON data from api.github.com/repos/keymanapp/keyman/pulls/#
# 3: Commit JSON data from api.github.com/repos/keymanapp/keyman/pulls/#/commits
#
function build_bot_check_messages() {
local PRNUM=$1
local prinfo="$2"
local prcommits="$3"
# We don't want any globbing happening in our parse
set -o noglob
# Extract the Build-bot commands from commit messages and the PR body
IFS=$'\n'
local buildBotCommands=($(echo "$prcommits" | "${JQ}" -r '.[].commit.message' | grep 'Build-bot:' | cut -d: -f 2 -))
local prCommands=($(echo "$prinfo" | "${JQ}" -r '.body' | tr -d '\r' | grep 'Build-bot:' | cut -d: -f 2 -))
unset IFS
# The PR body Build-bot comment will be read last, which allows it to override
# all previous commands
if [[ ${#prCommands[@]} -gt 0 ]]; then
buildBotCommands+=("${prCommands[@]}")
fi
for buildBotCommand in "${buildBotCommands[@]}"; do
# Block illegal Build-bot: commands
if [[ ! "$buildBotCommand" =~ ^[a-z,\ ]+$ ]]; then
builder_echo warning "WARNING[Build-bot]: ignoring invalid command: '${buildBotCommand}'"
continue
fi
# We now know that our command has only a-z, comma and space, so we can
# parse without risking escaping our bash jail
if [[ ! -z "${buildBotCommand// }" ]]; then
build_bot_update_commands $buildBotCommand
fi
done
set +o noglob
}
#
# parses a 'Build-bot: <level> [platform...]' command and modifies
# the global build_platforms associative array with new levels.
#
# Note that this function assumes that inputs are sanitized, see
# build_bot_check_messages
#
function build_bot_update_commands() {
local level=
local platforms=
local command="$*"
if [[ $# == 1 ]]; then
level=$1
platforms=all
else
level=$1
shift
# remaining parameters are comma/space separated platforms
IFS=', '
read -r -a platforms <<< "$*"
unset IFS
fi
if [[ ! $level =~ ^$valid_build_levels$ ]]; then
# Just skip this build command
builder_echo warning "WARNING[Build-bot]: ignoring invalid build level '$level' in command '$command'"
return 0
fi
build_bot_verify_platforms platforms
# a build command should be <level> [platform[, platform...]]
local platform
for platform in "${platforms[@]}"; do
builder_echo "Build-bot: Updating build level for $platform to $level"
build_platforms[$platform]=$level
done
}
#
# Strip any unrecognized platforms and expand 'all' to actual platforms
#
# Parameters:
# 1: name of platforms array parameter (byref)
#
function build_bot_verify_platforms() {
local -n input_platforms=$1
local output_platforms=()
local platform
for platform in "${input_platforms[@]}"; do
# We'll emit a warning with invalid platforms, then remove them from the array
if [[ ! $platform =~ ^(all|$available_platforms_regex)$ ]]; then
builder_echo warning "WARNING[Build-bot]: ignoring invalid platform '$platform'"
elif [[ $platform == all ]]; then
input_platforms=(${available_platforms[@]})
return
else
if [[ ! "${output_platforms[@]}" =~ $platform ]]; then
output_platforms+=($platform)
fi
fi
done
input_platforms=("${output_platforms[@]}")
}

View file

@ -1,10 +1,9 @@
#!/usr/bin/env bash
#
# Tell TeamCity to trigger new builds
# Tell TeamCity to trigger new release builds
#
function triggerBuilds() {
function triggerReleaseBuilds() {
local base=`git branch --show-current`
# convert stable-14.0 to stable_14_0 to fit in with the definitions
# in trigger-definitions.inc.sh
@ -19,39 +18,49 @@ function triggerBuilds() {
if [[ $build == "" ]]; then continue; fi
if [ "${build:(-7)}" == "_GitHub" ]; then
local job=${build%_GitHub}
echo Triggering GitHub action build "$job" "$base"
triggerGitHubActionsBuild false "$job" "$base"
echo "Triggering GitHub action release build (isTestBuild=false) (level=release) $job $base"
triggerGitHubActionsBuild false release "$job" "$base"
else
echo Triggering TeamCity build false $build $TEAMCITY_VCS_ID $base
triggerTeamCityBuild false $build $TEAMCITY_VCS_ID $base
echo "Triggering TeamCity release build (isTestBuild=false) (level=release) $build $TEAMCITY_VCS_ID $base"
triggerTeamCityBuild false release $build $TEAMCITY_VCS_ID $base
fi
done
done
}
#
# Trigger a build on TeamCity
#
# Parameters:
# 1: isTestBuild: 'true' if this is for a PR branch or a test build
# for a primary branch; false is this is a release
# build for a primary branch (master/beta/stable-x.y)
# 2: buildLevel: 'skip' - don't run build;
# 'build' - run build and unit tests;
# 'release' - run build, unit tests, and deploy
# 3: TeamCity build configuration name
# 4: TeamCity VCS id, which is different for PR test configurations
# 5: branch name in git
#
function triggerTeamCityBuild() {
local isTestBuild="$1"
local TEAMCITY_BUILDTYPE="$2"
local TEAMCITY_VCS_ID="$3"
local buildLevel="$2"
local TEAMCITY_BUILDTYPE="$3"
local TEAMCITY_VCS_ID="$4"
local TEAMCITY_BRANCH_NAME="$5"
if [[ $# -gt 3 ]]; then
local TEAMCITY_BRANCH_NAME="$4"
#debug echo " Triggering build for: $TEAMCITY_BRANCH_NAME"
TEAMCITY_BRANCH_NAME="branchName='$TEAMCITY_BRANCH_NAME' defaultBranch='false'"
else
local TEAMCITY_BRANCH_NAME=
fi
local GIT_OID=`git rev-parse HEAD`
local TEAMCITY_SERVER=https://build.palaso.org
local command
local commandProperties="<properties><property name='env.KEYMAN_BUILD_LEVEL' value='$buildLevel' /></properties>"
local commandLastChanges=
if $isTestBuild; then
command="<build $TEAMCITY_BRANCH_NAME><buildType id='$TEAMCITY_BUILDTYPE' /></build>"
else
command="<build $TEAMCITY_BRANCH_NAME><buildType id='$TEAMCITY_BUILDTYPE' /><lastChanges><change vcsRootInstance='$TEAMCITY_VCS_ID' locator='version:$GIT_OID'/></lastChanges></build>"
if ! $isTestBuild; then
local GIT_OID=`git rev-parse HEAD`
commandLastChanges="<lastChanges><change vcsRootInstance='$TEAMCITY_VCS_ID' locator='version:$GIT_OID'/></lastChanges>"
fi
local command="<build branchName='$TEAMCITY_BRANCH_NAME' defaultBranch='false'><buildType id='$TEAMCITY_BUILDTYPE' />$commandLastChanges$commandProperties</build>"
echo "TeamCity Build Command: $command"
# adjust indentation for output of curl
@ -66,10 +75,20 @@ function triggerTeamCityBuild() {
echo
}
#
# Trigger a build on GitHub Actions
#
# Parameters:
# 1: 'true' if this is for a PR branch, otherwise 'false' (for master/beta/stable-x.y)
# 2: 'skip' - don't run build; 'build' - run build and unit tests; 'test' - run build, unit tests, and deploy
# 3: Action name
# 4: branch name in git
#
function triggerGitHubActionsBuild() {
local IS_TEST_BUILD="$1"
local GITHUB_ACTION="$2"
local GIT_BRANCH="${3:-master}"
local BUILD_LEVEL="$2"
local GITHUB_ACTION="$3"
local GIT_BRANCH="$4"
local GIT_BASE_BRANCH="${GIT_BRANCH}"
local GIT_USER="keyman-server"
local GIT_BUILD_SHA GIT_BASE_REF JSON
@ -81,7 +100,8 @@ function triggerGitHubActionsBuild() {
GIT_BUILD_SHA="$(git rev-parse "refs/tags/release@$KEYMAN_VERSION_WITH_TAG")"
GIT_BASE_REF="$(git rev-parse "${GIT_BUILD_SHA}^")"
GIT_EVENT_TYPE="${GITHUB_ACTION}: release@${KEYMAN_VERSION_WITH_TAG}"
elif [[ $GIT_BRANCH != stable-* ]] && [[ $GIT_BRANCH =~ [0-9]+ ]]; then
elif [[ $GIT_BRANCH =~ ^[0-9]+$ ]]; then
# pull request
local JSON=$(call_curl "${GITHUB_SERVER}/pulls/${GIT_BRANCH}" --header "Authorization: token $GITHUB_TOKEN")
GIT_BUILD_SHA="$(echo "$JSON" | $JQ -r '.head.sha')"
GIT_EVENT_TYPE="${GITHUB_ACTION}: PR #${GIT_BRANCH}"
@ -90,6 +110,7 @@ function triggerGitHubActionsBuild() {
GIT_BASE_REF="$(echo "$JSON" | $JQ -r '.base.sha')"
GIT_BRANCH="PR-${GIT_BRANCH}"
else
# another branch: stable-x.y, beta, or master
GIT_BUILD_SHA="$(git rev-parse "refs/heads/${GIT_BRANCH}")"
GIT_BASE_REF="$(git rev-parse "${GIT_BUILD_SHA}^")"
GIT_EVENT_TYPE="${GITHUB_ACTION}: ${GIT_BRANCH}"
@ -103,7 +124,8 @@ function triggerGitHubActionsBuild() {
\"baseBranch\": \"$GIT_BASE_BRANCH\", \
\"baseRef\": \"$GIT_BASE_REF\", \
\"user\": \"$GIT_USER\", \
\"isTestBuild\": \"$IS_TEST_BUILD\" \
\"isTestBuild\": \"$IS_TEST_BUILD\", \
\"buildLevel\": \"$BUILD_LEVEL\" \
}}"
echo "GitHub Action Data: $DATA"

View file

@ -5,9 +5,39 @@
# Maps to ci/cancel-builds/trigger-definitions.mjs and must be kept in sync
#
#
# # Build levels
#
# See /docs/build-bot.md
#
readonly build_level_skip=skip
readonly build_level_build=build
readonly build_level_release=release
# TODO: future build_level_fulltest=fulltest --> do all expensive e2e tests as well as producing release artifacts
readonly valid_build_levels="$build_level_skip|$build_level_build|$build_level_release"
#
# Target platforms
#
available_platforms=(android common_web common_windows common_mac common_linux ios linux mac web windows developer)
# For test builds on master, beta, and stable-x.y branches, we always "build",
# not "release" -- that is, we don't produce artifacts
declare -Ag main_branch_platform_build_levels
for available_platforms_i in "${available_platforms[@]}"; do
main_branch_platform_build_levels[$available_platforms_i]=$build_level_build
done
readonly main_branch_platform_build_levels
readonly available_platforms
available_platforms_regex=`echo "${available_platforms[@]}" | sed 's/ /|/g'`
# We also allow 'common' and 'core' platforms for Build-bot: commands
readonly available_platforms_regex="$available_platforms_regex|common|core"
# the base folder for each pattern does not need to be included, nor oem folders
# e.g. android='common/models|common/predictive-text'
# will expand into android='^(android|(oem/[^/]+/android)|common/models|common/predictive-text)'
@ -57,6 +87,13 @@ bc_test_common_windows=(Keyman_Test_Common_Windows)
bc_test_common_mac=(Keyman_Test_Common_Mac)
bc_test_common_linux=(Keyman_Test_Common_Linux)
# These configuration arrays are triggered only by Build-bot commands:
bc_test_common=(Keyman_Test_Common_Web Keyman_Test_Common_Windows Keyman_Test_Common_Mac Keyman_Test_Common_Linux)
bc_test_core=(Keyman_Common_KPAPI_TestPullRequests_Linux Keyman_Common_KPAPI_TestPullRequests_macOS Keyman_Common_KPAPI_TestPullRequests_Windows)
# Core is tested directly for target platforms
# Keymanweb_TestPullRequestRegressions : currently this is timing out so disabled until we have
# time to investigate further

View file

@ -32,7 +32,7 @@ COMMON_ROOT="$KEYMAN_ROOT/common/windows/delphi"
OUTLIB="$WINDOWS_ROOT/lib"
COMMON_OUTLIB="$KEYMAN_ROOT/common/windows/lib"
if builder_is_debug_build || [[ $KEYMAN_VERSION_ENVIRONMENT == local ]] || [[ ! -z ${TEAMCITY_PR_NUMBER+x} ]]; then
if builder_is_debug_build || [[ $KEYMAN_VERSION_ENVIRONMENT == local ]] || builder_is_ci_test_build; then
# We do a fast build for debug builds, local builds, test PR builds but not for master/beta/stable release builds
GO_FAST=1
else
@ -73,6 +73,11 @@ run_in_delphi_env() {
}
sentrytool_delphiprep() {
if builder_is_ci_build && builder_is_ci_build_level_build; then
builder_echo "Skipping sentrytool_delphiprep - buildLevel=build: $@"
return 0
fi
local EXE_PATH="$1"
local DPR_PATH="$2"
(
@ -83,7 +88,7 @@ sentrytool_delphiprep() {
}
tds2dbg() {
"$TDS2DBG" "$@"
builder_if_release_build_level "$TDS2DBG" "$@"
}
delphi_msbuild() {
@ -108,6 +113,11 @@ clean_windows_project_files() {
}
wrap-signcode() {
if builder_is_ci_build && builder_is_ci_build_level_build; then
builder_echo "Skipping code signing - buildLevel=build: $@"
return 0
fi
# CI will usually pass in a full path for signtool.exe; for local builds we
# will hopefully find what we want on the path already
if [[ -z "${SIGNTOOL+x}" ]]; then
@ -117,6 +127,11 @@ wrap-signcode() {
}
wrap-symstore() {
if builder_is_ci_build && builder_is_ci_build_level_build; then
builder_echo "Skipping symstore - buildLevel=build"
return 0
fi
if [[ -z "${KEYMAN_SYMSTOREPATH+x}" ]]; then
builder_warn "\$KEYMAN_SYMSTOREPATH is not set. Skipping symstore for $@"
return 0

View file

@ -2221,6 +2221,47 @@ builder_is_ci_test_build() {
return 1
}
#
# Returns 0 if current ci build is a release-level build. Do not use for non-ci
# builds.
#
builder_is_ci_build_level_release() {
if builder_is_ci_release_build; then
return 0
fi
if [[ "$KEYMAN_BUILD_LEVEL" == release ]]; then
return 0
fi
return 1
}
#
# Returns 0 if current ci build is a build-level build. Do not use for non-ci
# builds.
#
builder_is_ci_build_level_build() {
if builder_is_ci_release_build; then
return 1
fi
if builder_is_ci_build_level_release; then
# KEYMAN_BUILD_LEVEL == release, i.e. not build
return 1
fi
return 0
}
#
# Executes statement if a ci build level of 'release', and for local builds, but
# not for a ci build level of 'build'
#
builder_if_release_build_level() {
if builder_is_ci_build && builder_is_ci_build_level_build; then
builder_echo "Skipping - buildLevel=build: $@"
return 0
fi
"$@"
}
################################################################################
# Final initialization
################################################################################

View file

@ -19,12 +19,17 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
builder_describe \
"Build Keyman Developer on Windows" \
"all run all actions" \
"all run all actions (used by TeamCity build configuration)" \
"build build Keyman Developer and test keyboards" \
"publish publish debug information files to sentry"
builder_parse "$@"
if ! is_windows; then
builder_echo error "This script is intended to be run on Windows only."
exit 1
fi
# shellcheck disable=SC2154
cd "${KEYMAN_ROOT}/developer/src"
@ -34,11 +39,11 @@ function build_developer_action() {
}
function _build_developer() {
builder_echo start "build developer" "Building Keyman Developer"
builder_echo start "build developer" "Building and testing Keyman Developer"
./build.sh configure build test api publish --dry-run
./build.sh configure build test
builder_echo end "build developer" success "Finished building Keyman Developer"
builder_echo end "build developer" success "Finished building and testing Keyman Developer"
}
function _build_testkeyboards() {
@ -50,21 +55,22 @@ function _build_testkeyboards() {
}
function publish_sentry_action() {
builder_echo start "publish" "Dry-run publish and api"
./build.sh api publish --dry-run
builder_echo end "publish" "Dry-run publish and api"
builder_echo start "publish sentry" "Publishing debug information files to Sentry"
# TODO: move this into build.sh publish? re-scope --dry-run into build.sh?
"${KEYMAN_ROOT}/developer/src/tools/sentry-upload-difs.sh"
builder_echo end "publish sentry" success "Finished publishing debug information files to Sentry"
}
if ! is_windows; then
builder_echo error "This script is intended to be run on Windows only."
exit 1
fi
if builder_has_action all; then
# TODO: control codesign by KEYMAN_BUILD_LEVEL
build_developer_action
publish_sentry_action
if builder_is_ci_build_level_release; then
publish_sentry_action
fi
else
builder_run_action build build_developer_action
builder_run_action publish publish_sentry_action

View file

@ -53,7 +53,7 @@ function do_build_desktop_resources() {
}
function do_publish() {
verify-program-signatures
builder_if_release_build_level verify-program-signatures
"$KEYMAN_ROOT/common/windows/cef-checkout.sh"
@ -73,6 +73,7 @@ function do_publish() {
-sice:ICE82 -sice:ICE80 \
-nologo \
-dWixUILicenseRtf=License.rtf \
"$WIXLIGHTCOMPRESSION" \
-out keymandesktop.msi -ext WixUIExtension \
keymandesktop.wixobj desktopui.wixobj cef.wixobj locale.wixobj
@ -106,7 +107,7 @@ function copy-installer() {
cp keymandesktop.exe "$KEYMAN_ROOT/windows/release/${KEYMAN_VERSION}/keyman-${KEYMAN_VERSION}.exe"
cp "$WINDOWS_PROGRAM_APP/setup.exe" "$KEYMAN_ROOT/windows/release/${KEYMAN_VERSION}/setup.exe"
verify-installer-signatures
builder_if_release_build_level verify-installer-signatures
# Copy the unsigned setup.exe for use in bundling scenarios; zip it up for clarity
wzzip "$KEYMAN_ROOT/windows/release/${KEYMAN_VERSION}/setup-redist.zip" "$WINDOWS_PROGRAM_APP/setup-redist.exe"

View file

@ -33,7 +33,7 @@ function do_build() {
tds2dbg "$WIN32_TARGET"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_APP"
cp "$WIN32_TARGET_PATH/insthelp.dbg" "$WINDOWS_DEBUGPATH_APP/insthelp.dbg"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/insthelp.dbg" "$WINDOWS_DEBUGPATH_APP/insthelp.dbg"
}
function do_publish() {

View file

@ -32,7 +32,7 @@ function do_build() {
tds2dbg "$WIN32_TARGET"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_APP"
cp "$WIN32_TARGET_PATH/kmbrowserhost.dbg" "$WINDOWS_DEBUGPATH_APP/kmbrowserhost.dbg"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmbrowserhost.dbg" "$WINDOWS_DEBUGPATH_APP/kmbrowserhost.dbg"
}
function do_publish() {

View file

@ -32,7 +32,7 @@ function do_build() {
tds2dbg "$WIN32_TARGET"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_APP"
cp "$WIN32_TARGET_PATH/kmconfig.dbg" "$WINDOWS_DEBUGPATH_APP/kmconfig.dbg"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmconfig.dbg" "$WINDOWS_DEBUGPATH_APP/kmconfig.dbg"
}
function do_publish() {

View file

@ -39,7 +39,7 @@ function do_build() {
tds2dbg "$WIN32_TARGET"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_APP"
cp "$WIN32_TARGET_PATH/kmshell.dbg" "$WINDOWS_DEBUGPATH_APP/kmshell.dbg"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmshell.dbg" "$WINDOWS_DEBUGPATH_APP/kmshell.dbg"
do_build_data
}

View file

@ -43,7 +43,7 @@ function do_build() {
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_APP"
# setup-redist.exe does not get signed as it is intended for a bundled installer
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_APP/setup-redist.exe"
cp "$WIN32_TARGET_PATH/setup.dbg" "$WINDOWS_DEBUGPATH_APP/setup.dbg"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/setup.dbg" "$WINDOWS_DEBUGPATH_APP/setup.dbg"
}
function do_build_debug_manifest() {

View file

@ -30,7 +30,7 @@ fi
#-------------------------------------------------------------------------------------------------------------------
function do_publish() {
verify-program-signatures
builder_if_release_build_level verify-program-signatures
#
# Build the installation archive
@ -38,7 +38,12 @@ function do_publish() {
"$WIXCANDLE" -dKEYMAN_VERSION=$KEYMAN_VERSION_WIN -dRELEASE=$KEYMAN_VERSION_RELEASE -ext WixUtilExtension keymanengine.wxs components.wxs
# warning 1072 relates to Error table defined by WixUtilExtension. Doesn't really affect us.
"$WIXLIGHT" -sw1072 -ext WixUtilExtension keymanengine.wixobj components.wixobj -o keymanengine.msm
"$WIXLIGHT" \
-sw1072 \
-ext WixUtilExtension \
"$WIXLIGHTCOMPRESSION" \
keymanengine.wixobj components.wixobj \
-o keymanengine.msm
#
# Sign the installation archive

View file

@ -31,7 +31,7 @@ function do_build() {
tds2dbg "$WIN32_TARGET"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_ENGINE"
cp "$WIN32_TARGET_PATH/insthelper.dbg" "$WINDOWS_DEBUGPATH_ENGINE/insthelper.dbg"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/insthelper.dbg" "$WINDOWS_DEBUGPATH_ENGINE/insthelper.dbg"
}
function do_publish() {

View file

@ -42,7 +42,7 @@ function do_build() {
tds2dbg "$WIN32_TARGET"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_ENGINE"
cp "$WIN32_TARGET_PATH/keyman.dbg" "$WINDOWS_DEBUGPATH_ENGINE/keyman.dbg"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/keyman.dbg" "$WINDOWS_DEBUGPATH_ENGINE/keyman.dbg"
# Also copy sentry files here
cp "$KEYMAN_ROOT/common/windows/delphi/ext/sentry/sentry.dll" \

View file

@ -41,7 +41,7 @@ function do_build_x86() {
build_version.res
vs_msbuild keyman32.vcxproj //t:Build "//p:Platform=Win32"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_ENGINE"
cp "$WIN32_TARGET_PATH/keyman32.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/keyman32.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
}
function do_build_x64() {
@ -49,7 +49,7 @@ function do_build_x64() {
run_in_vs_env rc version64.rc
vs_msbuild keyman32.vcxproj //t:Build "//p:Platform=x64"
cp "$X64_TARGET" "$WINDOWS_PROGRAM_ENGINE"
cp "$X64_TARGET_PATH/keyman64.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
builder_if_release_build_level cp "$X64_TARGET_PATH/keyman64.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
}
function do_publish_x86() {

View file

@ -33,7 +33,7 @@ function do_build() {
build_manifest.res
vs_msbuild keymanx64.vcxproj //t:Build "//p:Platform=x64"
cp "$X64_TARGET" "$WINDOWS_PROGRAM_ENGINE"
cp "$X64_TARGET_PATH/keymanx64.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
builder_if_release_build_level cp "$X64_TARGET_PATH/keymanx64.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
}
function do_publish() {

View file

@ -44,7 +44,7 @@ function do_build() {
mv -f "$WIN64_TARGET_PATH/kmcomapi.dll" "$WIN64_TARGET_PATH/kmcomapi.x64.dll"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_ENGINE"
cp "$WIN32_TARGET_PATH/kmcomapi.dbg" "$WINDOWS_DEBUGPATH_ENGINE/kmcomapi.dbg"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmcomapi.dbg" "$WINDOWS_DEBUGPATH_ENGINE/kmcomapi.dbg"
cp "$WIN64_TARGET_PATH/kmcomapi.x64.dll" "$WINDOWS_PROGRAM_ENGINE/kmcomapi.x64.dll"
# x64 Delphi symbols not supported: cp "$WIN64_TARGET_PATH/kmcomapi.dbg" "$WINDOWS_PROGRAM_ENGINE/kmcomapi.x64.dbg"

View file

@ -34,9 +34,9 @@ function do_build() {
vs_msbuild kmrefresh.vcxproj //t:Build "//p:Platform=Win32"
vs_msbuild kmrefresh.vcxproj //t:Build "//p:Platform=x64"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_ENGINE"
cp "$WIN32_TARGET_PATH/kmrefresh.x86.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmrefresh.x86.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
cp "$X64_TARGET" "$WINDOWS_PROGRAM_ENGINE"
cp "$X64_TARGET_PATH/kmrefresh.x64.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
builder_if_release_build_level cp "$X64_TARGET_PATH/kmrefresh.x64.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
}
function do_publish() {

View file

@ -35,9 +35,9 @@ function do_build() {
vs_msbuild kmtip.vcxproj //t:Build "//p:Platform=Win32"
vs_msbuild kmtip.vcxproj //t:Build "//p:Platform=x64"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_ENGINE"
cp "$WIN32_TARGET_PATH/kmtip.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/kmtip.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
cp "$X64_TARGET" "$WINDOWS_PROGRAM_ENGINE"
cp "$X64_TARGET_PATH/kmtip64.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
builder_if_release_build_level cp "$X64_TARGET_PATH/kmtip64.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
}
function do_publish() {

View file

@ -33,7 +33,7 @@ function do_build() {
build_manifest.res
vs_msbuild mcompile.vcxproj //t:Build "//p:Platform=Win32"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_ENGINE"
cp "$WIN32_TARGET_PATH/mcompile.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/mcompile.pdb" "$WINDOWS_DEBUGPATH_ENGINE"
}
function do_publish() {

View file

@ -47,7 +47,7 @@ function do_build() {
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_ENGINE"
cp *.xslt "$WINDOWS_PROGRAM_ENGINE/"
cp "$WIN32_TARGET_PATH/tsysinfo.dbg" "$WINDOWS_DEBUGPATH_ENGINE/tsysinfo.dbg"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/tsysinfo.dbg" "$WINDOWS_DEBUGPATH_ENGINE/tsysinfo.dbg"
}
function do_publish() {

View file

@ -31,7 +31,7 @@ function do_build() {
build_version.res
vs_msbuild etl2log.vcxproj //t:Build "//p:Platform=Win32"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_SUPPORT"
cp "$WIN32_TARGET_PATH/etl2log.pdb" "$WINDOWS_DEBUGPATH_SUPPORT"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/etl2log.pdb" "$WINDOWS_DEBUGPATH_SUPPORT"
}
builder_run_action clean:project do_clean

View file

@ -34,8 +34,8 @@ function do_build() {
vs_msbuild Editor.vcxproj //t:Build "//p:Platform=x64"
cp "$WIN32_TARGET" "$WINDOWS_PROGRAM_SUPPORT"
cp "$X64_TARGET" "$WINDOWS_PROGRAM_SUPPORT"
cp "$WIN32_TARGET_PATH/editor32.pdb" "$WINDOWS_DEBUGPATH_SUPPORT"
cp "$X64_TARGET_PATH/editor64.pdb" "$WINDOWS_DEBUGPATH_SUPPORT"
builder_if_release_build_level cp "$WIN32_TARGET_PATH/editor32.pdb" "$WINDOWS_DEBUGPATH_SUPPORT"
builder_if_release_build_level cp "$X64_TARGET_PATH/editor64.pdb" "$WINDOWS_DEBUGPATH_SUPPORT"
}
builder_run_action clean:project do_clean