From cf8b4a39c19c69aa823cddfd32f3d82fc12439da Mon Sep 17 00:00:00 2001 From: Peter Matkovski Date: Mon, 24 Aug 2026 17:05:01 +0200 Subject: [PATCH 1/6] ci: add least-privilege permissions to GitHub Actions workflows Add explicit workflow-level permissions blocks to resolve CodeQL actions/missing-workflow-permissions alerts. Scopes are derived from workflow operations. Refs: APPSEC-164 --- .github/workflows/ci.yml | 5 +++++ .github/workflows/initiate_release.yml | 4 ++++ .github/workflows/javadoc.yml | 4 ++++ .github/workflows/release.yml | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bcc6037..8a0e7463 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,11 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref }} cancel-in-progress: true +permissions: + actions: write + contents: read + pull-requests: read + jobs: build: name: ๐Ÿงช Test & lint diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml index 59c1c98c..462013c4 100644 --- a/.github/workflows/initiate_release.yml +++ b/.github/workflows/initiate_release.yml @@ -7,6 +7,10 @@ on: description: "The new version number with a 'v' prefix. Example: v1.40.1" required: true +permissions: + contents: write + pull-requests: write + jobs: init_release: name: ๐Ÿš€ Create release PR diff --git a/.github/workflows/javadoc.yml b/.github/workflows/javadoc.yml index 30938502..3ac7db41 100644 --- a/.github/workflows/javadoc.yml +++ b/.github/workflows/javadoc.yml @@ -3,6 +3,10 @@ on: push: branches: - main + +permissions: + contents: read + jobs: javadoc: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da5eba72..e0f3e517 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,6 +6,10 @@ on: branches: - main +permissions: + contents: read + pull-requests: read + jobs: Release: name: ๐Ÿš€ Release From 43be91be71c1916e992da0b7056c9ac857fe4a78 Mon Sep 17 00:00:00 2001 From: Peter Matkovski Date: Tue, 25 Aug 2026 09:25:39 +0200 Subject: [PATCH 2/6] ci: repair workflow permissions and runtimes --- .github/workflows/ci.yml | 9 ++++----- .github/workflows/initiate_release.yml | 6 +++--- .github/workflows/javadoc.yml | 22 +++++++++++----------- .github/workflows/release.yml | 9 ++++----- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a0e7463..488b39ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,6 @@ concurrency: cancel-in-progress: true permissions: - actions: write contents: read pull-requests: read @@ -17,19 +16,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' - name: Commit message lint - uses: wagoid/commitlint-github-action@v4 + uses: wagoid/commitlint-github-action@v6 - name: Restore cache - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*') }} diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml index 462013c4..f5710385 100644 --- a/.github/workflows/initiate_release.yml +++ b/.github/workflows/initiate_release.yml @@ -16,10 +16,10 @@ jobs: name: ๐Ÿš€ Create release PR runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 # gives the changelog generator access to all previous commits - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' @@ -36,7 +36,7 @@ jobs: git push -q -u origin "release-$VERSION" - name: Get changelog diff - uses: actions/github-script@v5 + uses: actions/github-script@v9 with: script: | const get_change_log_diff = require('./scripts/get_changelog_diff.js') diff --git a/.github/workflows/javadoc.yml b/.github/workflows/javadoc.yml index 3ac7db41..aa3fa739 100644 --- a/.github/workflows/javadoc.yml +++ b/.github/workflows/javadoc.yml @@ -5,29 +5,29 @@ on: - main permissions: - contents: read + contents: write jobs: javadoc: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: persist-credentials: false - - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' - name: Set up Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v6 with: node-version: 16 - name: Generate doc run: ./gradlew --no-daemon javadoc - name: Deploy - uses: JamesIves/github-pages-deploy-action@releases/v3 + uses: JamesIves/github-pages-deploy-action@v4 with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BRANCH: gh-pages - FOLDER: build/docs/javadoc/ + token: ${{ secrets.GITHUB_TOKEN }} + branch: gh-pages + folder: build/docs/javadoc/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e0f3e517..c7d70147 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,8 +7,7 @@ on: - main permissions: - contents: read - pull-requests: read + contents: write jobs: Release: @@ -16,14 +15,14 @@ jobs: if: github.event.pull_request.merged && startsWith(github.head_ref, 'release-') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' - - uses: actions/github-script@v5 + - uses: actions/github-script@v9 with: script: | const get_change_log_diff = require('./scripts/get_changelog_diff.js') From 44970c2e29979e59862c08295df972bcfdd6d5d9 Mon Sep 17 00:00:00 2001 From: Peter Matkovski Date: Tue, 25 Aug 2026 09:40:32 +0200 Subject: [PATCH 3/6] ci: repair moderation test policy --- .github/scripts/repair-moderation-policy.cjs | 120 +++++++++++++++++++ .github/workflows/ci.yml | 12 ++ 2 files changed, 132 insertions(+) create mode 100644 .github/scripts/repair-moderation-policy.cjs diff --git a/.github/scripts/repair-moderation-policy.cjs b/.github/scripts/repair-moderation-policy.cjs new file mode 100644 index 00000000..f1d12eab --- /dev/null +++ b/.github/scripts/repair-moderation-policy.cjs @@ -0,0 +1,120 @@ +const { StreamClient } = require('@stream-io/node-sdk'); + +const templateNames = [ + 'moderation_template_activity', + 'moderation_template_reaction', +]; +const rule = { name: 'profanity_en', action: 'remove' }; + +function withRequiredRule(blockListConfig = {}) { + const rules = [...(blockListConfig.rules || [])]; + const index = rules.findIndex((candidate) => candidate.name === rule.name); + + if (index === -1) { + rules.push(rule); + } else { + rules[index] = { ...rules[index], action: rule.action }; + } + + return { ...blockListConfig, rules }; +} + +function hasRequiredRule(blockListConfig) { + return blockListConfig?.rules?.some( + (candidate) => + candidate.name === rule.name && candidate.action === rule.action, + ); +} + +async function repairPolicy(client, key) { + const response = await client.moderation.getConfig({ key }); + const config = response.config; + if (!config) { + throw new Error(`Moderation policy ${key} was not found`); + } + if (hasRequiredRule(config.block_list_config)) { + console.log(`Moderation policy ${key} already contains the required rule`); + return; + } + + const writableFields = [ + 'async', + 'team', + 'ai_audio_config', + 'ai_image_config', + 'ai_text_config', + 'ai_video_config', + 'automod_platform_circumvention_config', + 'automod_semantic_filters_config', + 'automod_toxicity_config', + 'aws_rekognition_config', + 'block_list_config', + 'bodyguard_config', + 'flood_config', + 'google_vision_config', + 'llm_config', + 'rule_builder_config', + 'velocity_filter_config', + 'video_call_rule_config', + ]; + const payload = { key }; + for (const field of writableFields) { + if (config[field] !== undefined) { + payload[field] = config[field]; + } + } + payload.block_list_config = withRequiredRule(config.block_list_config); + + await client.moderation.upsertConfig(payload); + console.log(`Repaired moderation policy ${key}`); +} + +async function main() { + if (!process.env.STREAM_KEY || !process.env.STREAM_SECRET) { + throw new Error('STREAM_KEY and STREAM_SECRET are required'); + } + + const client = new StreamClient( + process.env.STREAM_KEY, + process.env.STREAM_SECRET, + ); + const response = await client.moderation.v2QueryTemplates(); + const templates = new Map( + response.templates.map((template) => [template.name, template]), + ); + const repairedPolicies = new Set(); + + for (const name of templateNames) { + const template = templates.get(name); + if (!template?.config) { + throw new Error(`Moderation template ${name} was not found`); + } + + if (template.config.config_key) { + if (!repairedPolicies.has(template.config.config_key)) { + await repairPolicy(client, template.config.config_key); + repairedPolicies.add(template.config.config_key); + } + continue; + } + + if (hasRequiredRule(template.config.block_list_config)) { + console.log(`Moderation template ${name} already contains the required rule`); + continue; + } + + await client.moderation.v2UpsertTemplate({ + name, + config: { + ...template.config, + block_list_config: withRequiredRule(template.config.block_list_config), + }, + }); + console.log(`Repaired moderation template ${name}`); + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +}); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 488b39ef..a3891742 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,18 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- + - uses: actions/setup-node@v6 + with: + node-version: '24' + + - name: Repair moderation test policy + env: + STREAM_KEY: ${{ secrets.STREAM_KEY }} + STREAM_SECRET: ${{ secrets.STREAM_SECRET }} + run: | + npm install --prefix "$RUNNER_TEMP/moderation-repair" --no-save --package-lock=false --ignore-scripts @stream-io/node-sdk@0.8.3 + NODE_PATH="$RUNNER_TEMP/moderation-repair/node_modules" node .github/scripts/repair-moderation-policy.cjs + - name: Test env: STREAM_KEY: ${{ secrets.STREAM_KEY }} From 5f2f12d573cffab523335aae46324b6c6e7c3124 Mon Sep 17 00:00:00 2001 From: Peter Matkovski Date: Tue, 25 Aug 2026 09:44:12 +0200 Subject: [PATCH 4/6] ci: ignore unavailable semantic policy --- .github/scripts/repair-moderation-policy.cjs | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/scripts/repair-moderation-policy.cjs b/.github/scripts/repair-moderation-policy.cjs index f1d12eab..0f0f9e1a 100644 --- a/.github/scripts/repair-moderation-policy.cjs +++ b/.github/scripts/repair-moderation-policy.cjs @@ -45,7 +45,6 @@ async function repairPolicy(client, key) { 'ai_text_config', 'ai_video_config', 'automod_platform_circumvention_config', - 'automod_semantic_filters_config', 'automod_toxicity_config', 'aws_rekognition_config', 'block_list_config', From 1be2ee8e56e68ff772b3d9b550cd7dadcbc6200c Mon Sep 17 00:00:00 2001 From: Peter Matkovski Date: Wed, 26 Aug 2026 13:50:25 +0200 Subject: [PATCH 5/6] ci: use dedicated moderation test blocklist --- .github/scripts/repair-moderation-policy.cjs | 38 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/.github/scripts/repair-moderation-policy.cjs b/.github/scripts/repair-moderation-policy.cjs index 0f0f9e1a..efeb4c83 100644 --- a/.github/scripts/repair-moderation-policy.cjs +++ b/.github/scripts/repair-moderation-policy.cjs @@ -4,10 +4,43 @@ const templateNames = [ 'moderation_template_activity', 'moderation_template_reaction', ]; -const rule = { name: 'profanity_en', action: 'remove' }; +const blockListName = 'stream_java_moderation_tests'; +const triggerWord = 'pissoar'; +const rule = { name: blockListName, action: 'remove' }; +const unavailableBlockListNames = new Set(['profanity_en']); + +async function ensureTestBlockList(client) { + const response = await client.listBlockLists(); + const blockList = response.blocklists.find( + (candidate) => candidate.name === blockListName, + ); + + if (!blockList) { + await client.createBlockList({ + name: blockListName, + type: 'word', + words: [triggerWord], + }); + console.log(`Created test blocklist ${blockListName}`); + return; + } + + if (!blockList.words.includes(triggerWord)) { + await client.updateBlockList({ + name: blockListName, + words: [...blockList.words, triggerWord], + }); + console.log(`Updated test blocklist ${blockListName}`); + return; + } + + console.log(`Test blocklist ${blockListName} already exists`); +} function withRequiredRule(blockListConfig = {}) { - const rules = [...(blockListConfig.rules || [])]; + const rules = (blockListConfig.rules || []).filter( + (candidate) => !unavailableBlockListNames.has(candidate.name), + ); const index = rules.findIndex((candidate) => candidate.name === rule.name); if (index === -1) { @@ -77,6 +110,7 @@ async function main() { process.env.STREAM_KEY, process.env.STREAM_SECRET, ); + await ensureTestBlockList(client); const response = await client.moderation.v2QueryTemplates(); const templates = new Map( response.templates.map((template) => [template.name, template]), From 4fa9a918230899d405eb061d86f41bbeaf52f520 Mon Sep 17 00:00:00 2001 From: Peter Matkovski Date: Wed, 26 Aug 2026 13:54:56 +0200 Subject: [PATCH 6/6] ci: retry moderation blocklist propagation --- .github/scripts/repair-moderation-policy.cjs | 43 ++++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/.github/scripts/repair-moderation-policy.cjs b/.github/scripts/repair-moderation-policy.cjs index efeb4c83..790fdddf 100644 --- a/.github/scripts/repair-moderation-policy.cjs +++ b/.github/scripts/repair-moderation-policy.cjs @@ -8,6 +8,29 @@ const blockListName = 'stream_java_moderation_tests'; const triggerWord = 'pissoar'; const rule = { name: blockListName, action: 'remove' }; const unavailableBlockListNames = new Set(['profanity_en']); +const propagationRetryDelaysMs = [1000, 2000, 4000, 8000]; + +function sleep(delayMs) { + return new Promise((resolve) => setTimeout(resolve, delayMs)); +} + +async function retryAfterBlockListPropagation(operation) { + for (const delayMs of propagationRetryDelaysMs) { + try { + return await operation(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (!message.includes(`Blocklist not found: ${blockListName}`)) { + throw error; + } + + console.log(`Waiting ${delayMs}ms for ${blockListName} to propagate`); + await sleep(delayMs); + } + } + + return operation(); +} async function ensureTestBlockList(client) { const response = await client.listBlockLists(); @@ -97,7 +120,9 @@ async function repairPolicy(client, key) { } payload.block_list_config = withRequiredRule(config.block_list_config); - await client.moderation.upsertConfig(payload); + await retryAfterBlockListPropagation(() => + client.moderation.upsertConfig(payload), + ); console.log(`Repaired moderation policy ${key}`); } @@ -136,13 +161,15 @@ async function main() { continue; } - await client.moderation.v2UpsertTemplate({ - name, - config: { - ...template.config, - block_list_config: withRequiredRule(template.config.block_list_config), - }, - }); + await retryAfterBlockListPropagation(() => + client.moderation.v2UpsertTemplate({ + name, + config: { + ...template.config, + block_list_config: withRequiredRule(template.config.block_list_config), + }, + }), + ); console.log(`Repaired moderation template ${name}`); } }