Skip to content
Open
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: 0 additions & 1 deletion .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,5 @@ jobs:
python -m pip install --user ansible-core==2.16.14
cd deploy
echo ${{ secrets.ANSIBLE_VAULT_PASSWORD }} > ansible-vault-password.txt
ansible-vault view --vault-password-file ansible-vault-password.txt files/jwt-signing-key.pem.enc > ../jwt-signing-key.pem
ansible-galaxy collection install ansible.posix
ansible-playbook provision.yml -e ansible_python_interpreter=/usr/bin/python3 --inventory inventory/production.yml
1 change: 0 additions & 1 deletion .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,5 @@ jobs:
python -m pip install --user ansible-core==2.16.14
cd deploy
echo ${{ secrets.ANSIBLE_VAULT_PASSWORD }} > ansible-vault-password.txt
ansible-vault view --vault-password-file ansible-vault-password.txt files/jwt-signing-key.pem.enc > ../jwt-signing-key.pem
ansible-galaxy collection install ansible.posix
ansible-playbook provision.yml -e ansible_python_interpreter=/usr/bin/python3 --inventory inventory/staging.yml
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,6 @@ server/migrations/test_plan_target_id.csv
server/migrations/dumps/dumpDatabase_*.sql
server/migrations/dumps/dumpTable_*.sql

# Private Key files (installed by deploy)
jwt-signing-key.pem

client/resources

#temp files for import-tests
Expand Down
89 changes: 0 additions & 89 deletions deploy/files/jwt-signing-key.pem.enc

This file was deleted.

8 changes: 0 additions & 8 deletions deploy/roles/application/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,6 @@
become: yes
register: environment_config

- name: Insert JWT signing key
copy:
dest: '{{source_dir}}/jwt-signing-key.pem'
src: files/jwt-signing-key.pem.enc
owner: '{{application_user}}'
become: yes
when: deployment_mode != 'development'

- name: Create database and database user
command: ./db/scripts/db_init.sh {{environment_config.dest}}
become: yes
Expand Down
14 changes: 9 additions & 5 deletions docs/automation.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Automation setup and configuration

This project makes use of an external github repository [aria-at-gh-actions-helper](https://github.com/bocoup/aria-at-gh-actions-helper) to launch github actions that run the automation suite.
More documentation about the tools used in that repository are in it's [README](https://github.com/bocoup/aria-at-gh-actions-helper/README.md).
This repository also has a second copy [aria-at-gh-actions-helper-dev](https://github.com/bocoup/aria-at-gh-actions-helper-dev) which is used in the staging or local environments by default to not get in the way of the queue for the live service.
This project makes use of an external github repository [aria-at-gh-actions-helper](https://github.com/w3c/aria-at-gh-actions-helper) to launch github actions that run the automation suite.
More documentation about the tools used in that repository are in it's [README](https://github.com/w3c/aria-at-gh-actions-helper/README.md).

## GitHub Workflow Automation Configuration

- The `jwt-signing-key.pem` file should be located in the project root folder. Obtain `ansible-vault-password.txt` from a project administrator and place it in the `deploy` folder. From within the `deploy` folder, you can run `ansible-vault view --vault-password-file ansible-vault-password.txt files/jwt-signing-key.pem.enc > ../jwt-signing-key.pem` to decrypt the configuration file.
- The `AUTOMATION_CALLBACK_FQDN` environment variable in the environment configuration file should be a **fully qualified domain name** that is accessible from the github workflow server pointing at the running instance of aria-at-app.
The following environment variables must be set in the environment configuration file (e.g. `deploy/files/config-staging.env`):

- `GITHUB_APP_ID` — The GitHub App ID for generating access tokens to dispatch workflows to the actions-helper repository, assigned by GitHub when the app is created.
- `GITHUB_APP_INSTALLATION_ID` — The installation ID for the GitHub App on the target organization.
- `GITHUB_APP_PRIVATE_KEY` — The GitHub App's PEM private key, base64-encoded: `base64 < private-key.pem`.
- `GITHUB_WORKFLOW_REPO` — The repository containing the workflow files (e.g. `w3c/aria-at-gh-actions-helper`).
- `AUTOMATION_CALLBACK_FQDN` — A **fully qualified domain name** that is accessible from the GitHub workflow server, pointing at the running instance of aria-at-app.
- For **local development** testing of these features, a forwarding proxy server like `ngrok` is recommended: `npx ngrok http 3000 --host-header=rewrite` will setup a server forwarding to your local 3000 development port. You can then use the domain it gives you when launching the app: `AUTOMATION_CALLBACK_FQDN=128935b17294.ngrok.app yarn dev`
3 changes: 2 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"homepage": "https://github.com/bocoup/aria-at-app#readme",
"dependencies": {
"@moebius/http-graceful-shutdown": "^1.1.0",
"@octokit/auth-app": "^8.2.0",
"@octokit/rest": "^22.0.1",
"apicache": "^1.6.3",
"apollo-server": "^3.13.0",
"apollo-server-core": "^3.13.0",
Expand All @@ -40,7 +42,6 @@
"fs-extra": "^11.2.0",
"graphql": "^16.9.0",
"js-base64": "^3.7.7",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"minimist": "^1.2.8",
"moment": "^2.30.1",
Expand Down
183 changes: 63 additions & 120 deletions server/services/GithubWorkflowService.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,67 @@
//import token from '../.github-app-token.json';
const axios = require('axios');
const fs = require('fs');
const jwt = require('jsonwebtoken');
const path = require('path');
const { Octokit } = require('@octokit/rest');
const { createAppAuth } = require('@octokit/auth-app');
const {
promises: { resolve }
} = require('dns');

const ONE_MINUTE = 60;

// Assigned by GitHub
const GITHUB_APP_ID = '395709';

// > your JWT must be signed using the RS256 algorithm.
//
// https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
const ALGORITHM = 'RS256';

// > Note: [The `workflow_dispatch event] will only trigger a workflow run if
// > the workflow file is on the default branch.
//
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
const WORKFLOW_REPO =
process.env.GITHUB_WORKFLOW_REPO ||
(process.env.ENVIRONMENT === 'production'
? 'bocoup/aria-at-gh-actions-helper'
: 'bocoup/aria-at-gh-actions-helper-dev');

// Generated from the GitHub.com UI
let privateKey = null;
const WORKFLOW_REPO = process.env.GITHUB_WORKFLOW_REPO;

let octokit = null;
let callbackUrlHostname = null;

exports.setup = async () => {
privateKey = fs.readFileSync(
path.join(__dirname, '../../jwt-signing-key.pem')
);
const appId = process.env.GITHUB_APP_ID;
if (!appId) {
throw new Error(
'Environment GITHUB_APP_ID must be set to the GitHub App ID.'
);
}

// The installation ID can be found via the GitHub API once the app is
// installed on an org: GET /orgs/{org}/installation
// https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
const installationId = process.env.GITHUB_APP_INSTALLATION_ID;
if (!installationId) {
throw new Error(
'Environment GITHUB_APP_INSTALLATION_ID must be set to the ' +
'GitHub App installation ID.'
);
}

// Provide the PEM key generated from the Github app. The value must be base64-encoded.
const rawKey = process.env.GITHUB_APP_PRIVATE_KEY;
if (!rawKey) {
throw new Error(
'Environment GITHUB_APP_PRIVATE_KEY must be set to the GitHub App PEM ' +
'private key (base64-encoded).'
);
}

// Decode the base64-encoded PEM key.
const privateKey = Buffer.from(rawKey, 'base64').toString('utf8');

if (!privateKey.includes('-----BEGIN')) {
throw new Error(
'GITHUB_APP_PRIVATE_KEY does not appear to be a valid base64-encoded ' +
'PEM key.'
);
}

// Octokit handles JWT creation, signing, and installation token
// exchange automatically via @octokit/auth-app.
octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId,
privateKey,
installationId
}
});

// strip possible https:// at start and trailing /
callbackUrlHostname = process.env.AUTOMATION_CALLBACK_FQDN?.replace(
/(^[^:]+:\/\/|\/$)/g,
Expand All @@ -57,66 +84,7 @@ exports.setup = async () => {
}
};

exports.isEnabled = () => privateKey && callbackUrlHostname;
// > 2. Get the ID of the installation that you want to authenticate as.
// >
// > If you are responding to a webhook event, the webhook payload will
// > include the installation ID.
// >
// > You can also use the REST API to find the ID for an installation of your
// > app. For example, you can get an installation ID with the `GET
// > /users/{username}/installation`, `GET
// > /repos/{owner}/{repo}/installation`, `GET /orgs/{org}/installation`, or
// > `GET /app/installations endpoints`. For more information, see
// > "[GitHub Apps](https://docs.github.com/en/rest/apps/apps)".
//
// https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
const GITHUB_APP_INSTALLATION_ID = '42217598';

// > The time that the JWT was created. To protect against clock drift, we
// > recommend that you set this 60 seconds in the past and ensure that your
// > server's date and time is set accurately (for example, by using the Network
// > Time Protocol).
//
// https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
const calculateIssuedAt = () => Math.round(Date.now() / 1000) - ONE_MINUTE;

const createJWT = (payload, privateKey, algorithm) => {
return new Promise((resolve, reject) => {
jwt.sign(payload, privateKey, { algorithm }, (err, token) => {
token ? resolve(token) : reject(err);
});
});
};

// > The expiration time of the JWT, after which it can't be used to request an
// > installation token. The time must be no more than 10 minutes into the
// > future.
//
// https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
const calculateExpiresAt = () => Math.round(Date.now() / 1000) + 9 * ONE_MINUTE;

const fetchInstallationAccessToken = async (jsonWebToken, installationID) => {
const response = await axios({
method: 'POST',
url: `https://api.github.com/app/installations/${installationID}/access_tokens`,
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${jsonWebToken}`,
'X-GitHub-Api-Version': '2022-11-28'
},
transformResponse: [],
validateStatus: () => true,
responseType: 'text'
});

if (response.status < 200 || response.status >= 300) {
throw new Error(
response.data ?? 'Unable to retrieve installation access token'
);
}
return JSON.parse(response.data).token;
};
exports.isEnabled = () => octokit && callbackUrlHostname;

/**
* We just want the whole number of the macOS version
Expand All @@ -142,16 +110,7 @@ const getWorkflowNameForMacOS = (atKey, atVersion) => {
};

const createGithubWorkflow = async ({ job, directory, gitSha, atVersion }) => {
const payload = {
iat: calculateIssuedAt(),
exp: calculateExpiresAt(),
iss: GITHUB_APP_ID
};
const jsonWebToken = await createJWT(payload, privateKey, ALGORITHM);
const accessToken = await fetchInstallationAccessToken(
jsonWebToken,
GITHUB_APP_INSTALLATION_ID
);
const [owner, repo] = WORKFLOW_REPO.split('/');

const atKey = job.testPlanRun.testPlanReport.at.key;
const workflowFilename = {
Expand Down Expand Up @@ -187,30 +146,14 @@ const createGithubWorkflow = async ({ job, directory, gitSha, atVersion }) => {
inputs.browser = browser;
inputs.jaws_version = atVersion?.name ?? 'latest';
}
const axiosConfig = {
method: 'POST',
url: `https://api.github.com/repos/${WORKFLOW_REPO}/actions/workflows/${workflowFilename}/dispatches`,

headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${accessToken}`,
'X-GitHub-Api-Version': '2022-11-28'
},
data: JSON.stringify({
ref: 'main',
inputs
}),
validateStatus: () => true,
responseType: 'text'
};
const response = await axios(axiosConfig);
if (response.status < 200 || response.status >= 300) {
throw new Error(
response.data
? JSON.stringify(response.data)
: 'Unable to initiate workflow'
);
}

await octokit.actions.createWorkflowDispatch({
owner,
repo,
workflow_id: workflowFilename,
ref: 'main',
inputs
});

return true;
};
Expand Down
Loading
Loading