Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ github-actions/google-internal-tests/main.js
github-actions/org-file-sync/main.js
github-actions/post-approval-changes/main.js
github-actions/previews/pack-and-upload-artifact/inject-artifact-metadata.js
github-actions/previews/pack-and-upload-artifact/remove-preview-label.js
github-actions/previews/upload-artifacts-to-firebase/extract-artifact-metadata.js
github-actions/previews/upload-artifacts-to-firebase/fetch-workflow-artifact.js
github-actions/saucelabs/set-saucelabs-env.js
Expand Down
14 changes: 14 additions & 0 deletions github-actions/previews/pack-and-upload-artifact/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ ts_project(
srcs = glob(["lib/*.ts"]),
tsconfig = "//github-actions:tsconfig",
deps = [
":node_modules/@actions/core",
":node_modules/@octokit/rest",
":node_modules/@types/node",
"//github-actions:utils",
"//github-actions/previews:constants_lib",
],
)
Expand All @@ -25,3 +28,14 @@ esbuild_checked_in(
platform = "node",
target = "node24",
)

esbuild_checked_in(
name = "remove-preview-label",
srcs = [
":lib",
],
entry_point = "lib/remove-preview-label.ts",
format = "esm",
platform = "node",
target = "node24",
)
22 changes: 22 additions & 0 deletions github-actions/previews/pack-and-upload-artifact/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,30 @@ inputs:
Project-relative path to the directory contents that should be deployed.
This is usually the distribution directory, like `dist/my-app/`.

angular-robot-key:
description: 'The private key for the Angular Robot Github app.'
required: true

triggering-label:
description: Label that triggers the preview deployment.
required: true

runs:
using: composite
steps:
- name: Automatically remove preview trigger label if PR author is not a Googler
if: contains(github.event.pull_request.labels.*.name, inputs.triggering-label)
shell: bash
env:
INPUT_ANGULAR-ROBOT-KEY: '${{inputs.angular-robot-key}}'
run: |
node ${{github.action_path}}/remove-preview-label.js \
'${{inputs.pull-number}}' \
'${{inputs.triggering-label}}'

- name: Copying artifact to temp directory to allow for metadata injection.
id: copy
if: contains(github.event.pull_request.labels.*.name, inputs.triggering-label)
Comment thread
josephperrott marked this conversation as resolved.
shell: bash
run: |
dir="$RUNNER_TEMP/pack-and-upload-tmp-dir/"
Expand All @@ -45,6 +64,7 @@ runs:
echo "deploy-dir=$dir" >> $GITHUB_OUTPUT

- name: Injecting artifact metadata
if: contains(github.event.pull_request.labels.*.name, inputs.triggering-label)
Comment thread
josephperrott marked this conversation as resolved.
shell: bash
run: |
node ${{github.action_path}}/inject-artifact-metadata.js \
Expand All @@ -54,6 +74,7 @@ runs:

- name: Creating compressed tarball of artifact
id: pack
if: contains(github.event.pull_request.labels.*.name, inputs.triggering-label)
Comment thread
josephperrott marked this conversation as resolved.
shell: bash
run: |
pkg="$RUNNER_TEMP/deploy-artifact.tar.gz"
Expand All @@ -62,6 +83,7 @@ runs:
echo "artifact-path=$pkg" >> $GITHUB_OUTPUT

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: contains(github.event.pull_request.labels.*.name, inputs.triggering-label)
Comment thread
josephperrott marked this conversation as resolved.
with:
name: '${{inputs.workflow-artifact-name}}'
path: '${{steps.pack.outputs.artifact-path}}'
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* @license
* Copyright Google LLC
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as core from '@actions/core';
import {Octokit} from '@octokit/rest';
import {ANGULAR_ROBOT, getAuthTokenFor, revokeActiveInstallationToken} from '../../../utils.js';

async function main() {
const [pullNumberRaw, labelName] = process.argv.slice(2);
const pullNumber = Number(pullNumberRaw);

if (isNaN(pullNumber)) {
throw new Error(`Invalid pull request number: ${pullNumberRaw}`);
}

let repoClient: Octokit | null = null;
let googlersOrgClient: Octokit | null = null;

try {
const [owner, repo] = process.env.GITHUB_REPOSITORY!.split('/', 2);

// Authenticate using the Angular Robot app for the repository
const repoToken = await getAuthTokenFor(ANGULAR_ROBOT, {owner, repo});
repoClient = new Octokit({auth: repoToken});

// Get pull request details to find the author
const pr = await repoClient.pulls.get({
owner,
repo,
pull_number: pullNumber,
});
const author = pr.data.user.login;

core.info(`PR #${pullNumber} author is: ${author}`);
Comment thread
josephperrott marked this conversation as resolved.

// Authenticate using the Angular Robot app for the googlers organization
const googlersOrgToken = await getGooglersOrgInstallationToken();
if (googlersOrgToken === null) {
throw new Error('Could not retrieve installation token for `googlers` org.');
}
googlersOrgClient = new Octokit({auth: googlersOrgToken});

const isMember = await isGooglerOrgMember(googlersOrgClient, author);

if (isMember) {
core.info(`PR author ${author} is a member of the googlers organization.`);
return;
}
Comment thread
josephperrott marked this conversation as resolved.

core.info(`PR author ${author} is NOT a member of the googlers organization.`);

// Check if the label is present on the PR before attempting to remove it
const labels = pr.data.labels.map((l) => l.name);
if (!labels.includes(labelName)) {
core.info(`Label "${labelName}" is not present on the PR.`);
return;
}
Comment thread
josephperrott marked this conversation as resolved.

core.info(`Removing label "${labelName}" from PR #${pullNumber}...`);
await repoClient.issues.removeLabel({
owner,
repo,
issue_number: pullNumber,
name: labelName,
});
core.info(`Successfully removed label "${labelName}"`);
Comment thread
josephperrott marked this conversation as resolved.
} finally {
if (googlersOrgClient !== null) {
try {
await revokeActiveInstallationToken(googlersOrgClient);
} catch (e) {
core.error(`Failed to revoke googlers org token: ${e}`);
}
}
if (repoClient !== null) {
try {
await revokeActiveInstallationToken(repoClient);
} catch (e) {
core.error(`Failed to revoke repo token: ${e}`);
}
}
}
Comment thread
josephperrott marked this conversation as resolved.
}

async function getGooglersOrgInstallationToken(): Promise<string | null> {
try {
return await getAuthTokenFor(ANGULAR_ROBOT, {
org: 'googlers',
});
} catch (e) {
core.error('Could not retrieve installation token for `googlers` org.');
core.error(e as Error);
}
return null;
}

const isGooglerOrgMemberCache = new Map<string, boolean>();

async function isGooglerOrgMember(client: Octokit, username: string): Promise<boolean> {
if (isGooglerOrgMemberCache.has(username)) {
return isGooglerOrgMemberCache.get(username)!;
}
return await client.orgs
.checkMembershipForUser({org: 'googlers', username})
.then(
({status}) => (status as number) === 204,
() => false,
)
.then((result) => {
isGooglerOrgMemberCache.set(username, result);
return result;
});
}
Comment thread
josephperrott marked this conversation as resolved.

main().catch((e) => {
core.setFailed(e.message);
});
2 changes: 2 additions & 0 deletions github-actions/previews/pack-and-upload-artifact/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"dependencies": {
"@actions/core": "3.0.1",
"@octokit/rest": "22.0.1",
"@types/node": "24.12.4"
}
}
Loading