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
36 changes: 36 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Tests

on:
pull_request:
push:
branches: [master, develop]

concurrency:
group: tests-${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
test:
name: Node ${{ matrix.node }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# 18 is the floor declared in package.json engines
node: [18, 20, 22]

steps:
- uses: actions/checkout@v7

- uses: pnpm/action-setup@v6
with:
version: 10.7.1

- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: pnpm

- run: pnpm install --frozen-lockfile
Comment thread
xolott-ark marked this conversation as resolved.

- run: pnpm test
23 changes: 19 additions & 4 deletions src/actions/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,14 @@ async function checkCorrectOrganisation(orgUuid, opts) {
const authData = await api({
path: '/authenticate',
});
if (authData.organisationUuid !== organisationUuid) {
// /authenticate returns identity flat at the top level. It also has a
// `data` key, but that holds OAuth authorization metadata
// (type/scopes/appUuid/authorizationUuid), not identity, so it must not
// be treated as a response envelope.
const authBody = authData ?? {};
if (authBody.organisationUuid !== organisationUuid) {
log(
`This configuration is for organisation ${organisationUuid} but you are currently in organisation ${authData.organisationUuid}`,
`This configuration is for organisation ${organisationUuid} but you are currently in organisation ${authBody.organisationUuid}`,
'white'
);
const response = await inquirer.prompt([
Expand All @@ -46,12 +51,22 @@ async function checkCorrectOrganisation(orgUuid, opts) {
},
]);
if (response.confirm) {
// Continuing here would run the command against the
// organisation the user just declined, so abort rather than
// returning.
if (!authBody.userUuid) {
log(
'Your session does not identify a user, so the CLI cannot switch organisations for you. Switch organisation in the Raisely admin, then run raisely init again.',
'red'
);
process.exit(-1);
}
const loader = ora(
'Switching to correct organisation ...'
).start();
try {
await api({
path: `/users/${authData.userUuid}/move`,
path: `/users/${authBody.userUuid}/move`,
method: 'PUT',
json: {
data: {
Expand All @@ -62,7 +77,7 @@ async function checkCorrectOrganisation(orgUuid, opts) {
loader.succeed();
} catch (e) {
error(e, loader);
throw e;
process.exit(-1);
}
}
}
Expand Down
45 changes: 40 additions & 5 deletions src/actions/components.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import api from './api.js';
import { loadBabelCore } from './babel.js';
import { loadConfig } from '../config.js';

import path from 'path';
import fs from 'fs';
Expand All @@ -15,19 +16,53 @@ async function getComponent(uuid, opts = {}) {
});
}

async function resolveOrganisationUuid() {
const config = await loadConfig({ allowEmpty: true });
if (config.organisationUuid) {
return config.organisationUuid;
}

try {
const authData = await api({
path: '/authenticate',
});
// /authenticate returns organisationUuid flat at the top level; its
// `data` key holds OAuth authorization metadata, not identity.
// `/users/me` is not an option here: CLI tokens are app
// authorizations with no user record, so it resolves `me` to the
// authorization uuid and 404s.
if (authData?.organisationUuid) {
return authData.organisationUuid;
}
} catch {
// fall through to actionable error below
}

return null;
}

const ORGANISATION_RESOLUTION_ERROR = [
'The CLI could not resolve your Raisely organisation.',
'Try signing out and back in, then re-initialize this directory if needed:',
' raisely logout',
' raisely login',
' raisely init',
'If you already have a .raisely.json here, make sure it includes organisationUuid (re-run raisely init to refresh it).',
].join('\n');

export async function createComponent({ name, apiUrl }, opts = {}) {
// fetch the organisation ID
const user = await api({
path: '/users/me',
});
const organisationUuid = await resolveOrganisationUuid();
if (!organisationUuid) {
throw ORGANISATION_RESOLUTION_ERROR;
}

return await api({
path: `/components?private=1`,
method: 'POST',
json: {
data: {
name,
organisationUuid: user.data.organisationUuid,
organisationUuid,
},
},
});
Expand Down
23 changes: 21 additions & 2 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,27 @@ export async function informLocalDev(config) {
const authData = await api({
path: '/authenticate',
});
const organisation = authData.data.organisation;
if (!organisation.private || !organisation.private.localDevelopment) {
// /authenticate carries identity flat at the top level but no organisation
// record, so the localDevelopment flag has to be read from the org itself.
const organisationUuid =
config?.organisationUuid || authData?.organisationUuid;

let organisation;
if (organisationUuid) {
try {
const orgResponse = await api({
path: `/organisations/${organisationUuid}?private=1`,
});
organisation = orgResponse?.data;
} catch (e) {
// No OAuth app scope grants reading an organisation record, so this
// is a 403 for any CLI login and only succeeds for admin tokens
// supplied via RAISELY_TOKEN. The flag is advisory, so skip the
// warning rather than blocking the command.
}
}

if (!organisation?.private?.localDevelopment) {
// this is fine, we can continue without warning
return true;
}
Expand Down
67 changes: 65 additions & 2 deletions src/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from './actions/layout.js';

import { uploadStyles } from './actions/campaigns.js';
import { uploadPage } from './actions/pages.js';
import {
updateComponentFile,
updateComponentConfig,
Expand All @@ -32,21 +33,80 @@ function startLoader(message, oraImpl = ora) {
return oraImpl(message).start();
}

/**
* Upload a single page JSON, applying the same guards as `raisely deploy`:
* the page must have a uuid and belong to a configured campaign.
*/
async function uploadChangedPage(
filenameRaw,
relative,
{ config, fsModule, uploadPageFn, oraFn, errorFn }
) {
const loader = startLoader(`Saving ${relative}`, oraFn);

let pageData;
try {
pageData = JSON.parse(fsModule.readFileSync(filenameRaw, 'utf8'));
} catch (e) {
loader.fail(`${relative} is not valid JSON, skipping upload`);
return;
}

if (!pageData.uuid) {
loader.fail(`${relative} has no uuid, skipping upload`);
return;
}

const campaigns = config?.campaigns ?? [];
if (
!pageData.campaignUuid ||
!campaigns.includes(pageData.campaignUuid)
) {
loader.fail(
`${relative} does not belong to a configured campaign, skipping upload`
);
return;
}

try {
await uploadPageFn(pageData);
loader.succeed();
} catch (e) {
errorFn(e, loader);
}
}

export async function handleCampaignChange(
filenameRaw,
{
campaignsDir,
token,
config,
fsModule = fs,
uploadStylesFn = uploadStyles,
uploadPageFn = uploadPage,
validateCampaignSassFn = validateCampaignSass,
oraFn = ora,
errorFn = error,
} = {}
) {
const relative = path.relative(campaignsDir, filenameRaw);
const parts = relative.split(path.sep);
// Only handle stylesheet changes: <campaign-path>/stylesheets/...
if (parts.length < 3 || parts[1] !== 'stylesheets') return;
if (parts.length < 3) return;
const campaignPath = parts[0];

if (parts[1] === 'pages' && relative.endsWith('.json')) {
return await uploadChangedPage(filenameRaw, relative, {
config,
fsModule,
uploadPageFn,
oraFn,
errorFn,
});
}

// Anything else under a campaign other than stylesheets is not uploaded
if (parts[1] !== 'stylesheets') return;
const loader = startLoader(`Saving ${relative}`, oraFn);
const validation = await validateCampaignSassFn({
campaign: campaignPath,
Expand Down Expand Up @@ -143,9 +203,12 @@ export function registerStartWatchers(
await handleCampaignChange(filenameRaw, {
campaignsDir,
token: config.token,
config,
uploadStylesFn: dependencies.uploadStylesFn,
uploadPageFn: dependencies.uploadPageFn,
validateCampaignSassFn: dependencies.validateCampaignSassFn,
oraFn: dependencies.oraFn,
errorFn: dependencies.errorFn,
});
}
);
Expand Down
Loading