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
18 changes: 18 additions & 0 deletions .github/workflows/_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,23 @@ jobs:
- name: Install app dependencies
run: npm ci

# Manifest variables: substituted into manifest.yml by the Forge CLI from
# the shell at deploy time. Only override the SaaS defaults when provided.
- name: Set manifest variables
run: |
if [ -n "${{ vars.FORGE_APP_ID }}" ]; then
echo "FORGE_APP_ID=${{ vars.FORGE_APP_ID }}" >> "$GITHUB_ENV"
fi
if [ -n "${{ vars.FLAGSMITH_BACKEND_HOST }}" ]; then
echo "FLAGSMITH_BACKEND_HOST=${{ vars.FLAGSMITH_BACKEND_HOST }}" >> "$GITHUB_ENV"
fi
# Runtime variables: stored by Forge and read via process.env in functions.
# Always set them; an empty value is falsy, so the code's `|| DEFAULT`
# fallback yields the SaaS defaults just as an unset variable would.
- name: Set runtime variables
run: |
forge variables set FLAGSMITH_API_V1 "${{ vars.FLAGSMITH_API_V1 }}" --environment ${{ inputs.environment }} --non-interactive
forge variables set FLAGSMITH_APP "${{ vars.FLAGSMITH_APP }}" --environment ${{ inputs.environment }} --non-interactive

- name: Deploy
run: forge deploy --environment ${{ inputs.environment }} --non-interactive
10 changes: 8 additions & 2 deletions flagsmith-jira-app/manifest.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
app:
id: ari:cloud:ecosystem::app/9ea1f892-1356-44bd-9ac8-684abf3a5714
id: ari:cloud:ecosystem::app/${FORGE_APP_ID}
runtime:
name: nodejs22.x
modules:
Expand Down Expand Up @@ -54,4 +54,10 @@ permissions:
external:
fetch:
backend:
- api.flagsmith.com
- ${FLAGSMITH_BACKEND_HOST}
environment:
variables:
- key: FORGE_APP_ID
default: 9ea1f892-1356-44bd-9ac8-684abf3a5714
- key: FLAGSMITH_BACKEND_HOST
default: api.flagsmith.com
15 changes: 14 additions & 1 deletion flagsmith-jira-app/src/backend/flagsmith.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import api, { APIResponse, Route, assumeTrustedRoute, route } from "@forge/api";

import { ApiArgs, ApiError, FLAGSMITH_API_V1 } from "../common";
import {
ApiArgs,
ApiError,
DEFAULT_FLAGSMITH_API_V1,
DEFAULT_FLAGSMITH_APP,
ReadConfig,
} from "../common";
import { readApiKey, readOrganisationId } from "./storage";

const FLAGSMITH_API_V1 = process.env.FLAGSMITH_API_V1 || DEFAULT_FLAGSMITH_API_V1;

type Model = {
id: number;
name: string;
Expand Down Expand Up @@ -241,3 +249,8 @@ export const readEnvironmentFeatureState: ReadEnvironmentFeatureState = async ({
return results[0] as EnvironmentFeatureState;
}
};

/** Read frontend-visible configuration from runtime environment variables */
export const readConfig: ReadConfig = async () => ({
flagsmithApp: process.env.FLAGSMITH_APP || DEFAULT_FLAGSMITH_APP,
});
14 changes: 11 additions & 3 deletions flagsmith-jira-app/src/common.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { RequestInit } from "@forge/api";
import { useEffect, useState } from "react";

// TODO later: these could be set from environment variables for self-hosted users
export const FLAGSMITH_API_V1 = "https://api.flagsmith.com/api/v1";
export const FLAGSMITH_APP = "https://app.flagsmith.com";
// SaaS defaults; overridden per Forge environment via the FLAGSMITH_API_V1 and
// FLAGSMITH_APP runtime variables (see src/backend/config.ts).
export const DEFAULT_FLAGSMITH_API_V1 = "https://api.flagsmith.com/api/v1";
export const DEFAULT_FLAGSMITH_APP = "https://app.flagsmith.com";

export type ApiArgs = Partial<{
method: RequestInit["method"];
Expand All @@ -24,6 +25,13 @@ export class ApiError extends Error {

export type ErrorPayload = { error: Error };

/** Frontend-visible configuration passed back across the invoke bridge */
export type Config = {
flagsmithApp: string;
};

export type ReadConfig = () => Promise<Config>;

/** Use a promise safely i.e. ignore result if subsequently unmounted */
export const usePromise = <T>(
promise: () => Promise<T>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
} from "@forge/react";
import { Fragment, useCallback, useState } from "react";

import { FLAGSMITH_APP, usePromise } from "../../common";
import { DEFAULT_FLAGSMITH_APP, usePromise } from "../../common";
import { readConfig } from "../flagsmith";
import {
Environment,
EnvironmentFeatureState,
Expand Down Expand Up @@ -133,11 +134,13 @@ type IssueFeatureTableProps = {
environments: Environment[]; // must be non-empty
// list of same feature in the context of each environment
environmentFeatures: Feature[];
flagsmithApp: string;
};

const IssueFeatureTable = ({
environments,
environmentFeatures,
flagsmithApp,
}: IssueFeatureTableProps): JSX.Element => {
// catch API errors per table rather than cause whole component to fail
const [error, setError] = useState<Error>();
Expand All @@ -146,7 +149,7 @@ const IssueFeatureTable = ({
const featureId = environmentFeatures[0]!.id;
const featureName = environmentFeatures[0]!.name;
const featureProjectId = environmentFeatures[0]!.project;
const projectUrl = `${FLAGSMITH_APP}/project/${featureProjectId}`;
const projectUrl = `${flagsmithApp}/project/${featureProjectId}`;

/** Read feature state for each environment */
const readFeatureState = useCallback(async (): Promise<FeatureState> => {
Expand Down Expand Up @@ -294,6 +297,8 @@ const IssueFeatureTables = ({
environmentsFeatures,
issueFeatureIds,
}: IssueFeatureTablesProps): JSX.Element => {
const [config = { flagsmithApp: DEFAULT_FLAGSMITH_APP }] = usePromise(readConfig, []);

if (
environmentsFeatures.length === 0 ||
environmentsFeatures[0] === undefined ||
Expand Down Expand Up @@ -329,6 +334,7 @@ const IssueFeatureTables = ({
<IssueFeatureTable
environments={matchingEnvironments}
environmentFeatures={envFeaturesForThisFeature}
flagsmithApp={config.flagsmithApp}
/>
</Fragment>
),
Expand Down
4 changes: 4 additions & 0 deletions flagsmith-jira-app/src/frontend/flagsmith.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ReadOrganisations,
ReadProjects,
} from "../backend/flagsmith";
import type { ReadConfig } from "../common";
import { customInvoke } from "./invoke";
export {
Environment,
Expand Down Expand Up @@ -35,3 +36,6 @@ export const readEnvironmentFeatureState: ReadEnvironmentFeatureState = async ({
envApiKey,
featureName,
}) => customInvoke("readEnvironmentFeatureState", { envApiKey, featureName });

/** Read frontend-visible configuration from the backend resolver */
export const readConfig: ReadConfig = async () => customInvoke("readConfig");
3 changes: 3 additions & 0 deletions flagsmith-jira-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Resolver from "@forge/resolver";

import { canEditIssue, canAdministrate, canAdministrateProject } from "./backend/auth";
import {
readConfig,
readEnvironmentFeatureState,
readEnvironments,
readFeatures,
Expand Down Expand Up @@ -31,6 +32,8 @@ const resolver = new Resolver();

resolver.define("canEditIssue", async ({ payload }) => canEditIssue(payload).catch(returnError));

resolver.define("readConfig", async () => readConfig().catch(returnError));

const checkPermission = async (hasPermission: () => Promise<boolean>) => {
if (!(await hasPermission())) throw new ApiError("Forbidden", 403);
};
Expand Down
Loading