diff --git a/.github/workflows/_deploy.yml b/.github/workflows/_deploy.yml index b8d5796..a765a12 100644 --- a/.github/workflows/_deploy.yml +++ b/.github/workflows/_deploy.yml @@ -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 diff --git a/flagsmith-jira-app/manifest.yml b/flagsmith-jira-app/manifest.yml index 384d175..9d310d8 100644 --- a/flagsmith-jira-app/manifest.yml +++ b/flagsmith-jira-app/manifest.yml @@ -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: @@ -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 diff --git a/flagsmith-jira-app/src/backend/flagsmith.ts b/flagsmith-jira-app/src/backend/flagsmith.ts index c0895c3..5799891 100644 --- a/flagsmith-jira-app/src/backend/flagsmith.ts +++ b/flagsmith-jira-app/src/backend/flagsmith.ts @@ -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; @@ -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, +}); diff --git a/flagsmith-jira-app/src/common.ts b/flagsmith-jira-app/src/common.ts index f4a05c0..20ce911 100644 --- a/flagsmith-jira-app/src/common.ts +++ b/flagsmith-jira-app/src/common.ts @@ -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"]; @@ -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; + /** Use a promise safely i.e. ignore result if subsequently unmounted */ export const usePromise = ( promise: () => Promise, diff --git a/flagsmith-jira-app/src/frontend/components/IssueFeatureTables.tsx b/flagsmith-jira-app/src/frontend/components/IssueFeatureTables.tsx index d7d787b..d1d49ee 100644 --- a/flagsmith-jira-app/src/frontend/components/IssueFeatureTables.tsx +++ b/flagsmith-jira-app/src/frontend/components/IssueFeatureTables.tsx @@ -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, @@ -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(); @@ -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 => { @@ -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 || @@ -329,6 +334,7 @@ const IssueFeatureTables = ({ ), diff --git a/flagsmith-jira-app/src/frontend/flagsmith.ts b/flagsmith-jira-app/src/frontend/flagsmith.ts index 7ac03c6..208685e 100644 --- a/flagsmith-jira-app/src/frontend/flagsmith.ts +++ b/flagsmith-jira-app/src/frontend/flagsmith.ts @@ -5,6 +5,7 @@ import { ReadOrganisations, ReadProjects, } from "../backend/flagsmith"; +import type { ReadConfig } from "../common"; import { customInvoke } from "./invoke"; export { Environment, @@ -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"); diff --git a/flagsmith-jira-app/src/index.ts b/flagsmith-jira-app/src/index.ts index 690bbe9..d576de6 100644 --- a/flagsmith-jira-app/src/index.ts +++ b/flagsmith-jira-app/src/index.ts @@ -2,6 +2,7 @@ import Resolver from "@forge/resolver"; import { canEditIssue, canAdministrate, canAdministrateProject } from "./backend/auth"; import { + readConfig, readEnvironmentFeatureState, readEnvironments, readFeatures, @@ -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) => { if (!(await hasPermission())) throw new ApiError("Forbidden", 403); };