Skip to content

Commit 8e4d781

Browse files
WIP: new oauth plugin for NBS to replace backend plugin (#159)
* chore: add Backstage yarn plugin to prep for upgrade * docs: add comment about new gitignore settings * fix: run 'yarn install' and save state to branch * chore: try updating more stuff * chore: clean up backend package to resemble NBS from fresh install * docs: add missing comment to app-config * chore: clean up backend package further * fix: remove typo in app-config * refactor: switch to prop for sign in page * chore: get initial GH auth flow working * chore: remove deprecated backend packages from package.json * chore: more config clean up * fix: update .gitignore * chore: add package.json comment * chore: add more comments * chore: add note about coder plugin * docs: add more code snippets * refactor: migrate backend coder plugin to NBS * chore: add in example data used by newer scaffolded apps * fix: improve logging granularity for coder backend plugin * fix: revert logging change * wip: commit progress on devcontainers-backend conversion * wip: consolidate temp package back into main one * chore: add devcontainers-backend to the sample app * fix: resolve version mismatch in package.json * chore: update all files at root of directory * fix: revert accidental change to devcontainers README * chore: update basic frontend files * chore: update rendering output for app directory * chore: update Root component * chore: update EntityPage * fix: make sure kubernetes config is null * fix: update App page to use newer APIs * fix: remove fallback values for .env * fix: add back missing themes for legacy MUI code * chore: update react imports for devcontainers-frontend * fix: add missing dependency for devcontainers backend plugin * chore: remove all React imports from project * fix: silence warning about package type mismatch * refactor: clean up kludge * fix: format two yaml files * feat: migrate Coder plugin to Backstage NBS with OAuth integration * feat: integrate Coder OAuth2 provider with Backstage NBS * feat: resolve remaining linting and test issues. * fix: re-include Yarn releases directory in .gitignore * chore: yarn prettier * chore: update dependencies and versions * chore: simplify build step for devcontainers-react plugin * chore: re-order build step * chore: resolve remaining blocking issues * chore: update yarn version command in CI workflow * chore: update versioning command in CI workflow to use npm * chore: modify versioning in CI workflow to set package version based on short SHA * chore: add pluginId and pluginPackages to backstage plugin configurations * chore: update CI workflow to pack plugin with custom output filename * chore: add configApi to OAuth2 provider configuration * chore: update README and CI workflow for plugin release tagging * chore: yarn prettier * chore: clean up whitespace in release workflow --------- Co-authored-by: Michael Smith <[email protected]>
1 parent eb19112 commit 8e4d781

File tree

141 files changed

+39959
-25157
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

141 files changed

+39959
-25157
lines changed

.eslintrc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
module.exports = {
22
root: true,
3+
rules: {
4+
// React 17+ JSX transform doesn't require React to be in scope
5+
'react/react-in-jsx-scope': 'off',
6+
},
37
};

.github/workflows/release.yaml

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
name: Release
22

3-
# This workflow will draft a release for a plugin when tagged. The tag format
4-
# is <name>/v<version> without the backstage-plugin- prefix, e.g. coder/v0.0.0
3+
# This workflow will draft a release for a plugin when tagged. The tag format
4+
# is <plugin-directory>/v<version> where plugin-directory is the exact directory
5+
# name under plugins/, e.g.:
6+
# - backstage-plugin-coder/v0.0.0
7+
# - auth-backend-module-coder-provider/v0.0.0
8+
# - backstage-plugin-devcontainers-backend/v0.0.0
59

610
on:
711
push:
@@ -20,19 +24,32 @@ jobs:
2024
plugin: ${{ steps.split.outputs.plugin }}
2125
version: ${{ steps.split.outputs.version }}
2226
steps:
27+
- uses: actions/checkout@v4
2328
- env:
2429
TAG: ${{ github.ref_name }}
2530
id: split
2631
run: |
2732
parts=(${TAG//\/v/ })
28-
echo "plugin=${parts[0]}" >> $GITHUB_OUTPUT
29-
echo "version=${parts[1]}" >> $GITHUB_OUTPUT
33+
PLUGIN_NAME="${parts[0]}"
34+
VERSION="${parts[1]}"
35+
36+
if [[ ! -d "plugins/${PLUGIN_NAME}" ]]; then
37+
echo "Error: Plugin directory 'plugins/${PLUGIN_NAME}' not found"
38+
echo "Tag should match the exact directory name in plugins/"
39+
exit 1
40+
fi
41+
42+
echo "plugin=${PLUGIN_NAME}" >> $GITHUB_OUTPUT
43+
echo "version=${VERSION}" >> $GITHUB_OUTPUT
44+
45+
echo "Plugin: ${PLUGIN_NAME}"
46+
echo "Version: ${VERSION}"
3047
plugin:
3148
needs: split-tag
3249
runs-on: ubuntu-latest
3350
defaults:
3451
run:
35-
working-directory: plugins/backstage-plugin-${{ needs.split-tag.outputs.plugin }}
52+
working-directory: plugins/${{ needs.split-tag.outputs.plugin }}
3653
name: ${{ needs.split-tag.outputs.plugin }} v${{ needs.split-tag.outputs.version }}
3754
steps:
3855
- uses: actions/checkout@v4
@@ -51,4 +68,4 @@ jobs:
5168
- uses: softprops/action-gh-release@v2
5269
with:
5370
draft: true
54-
files: plugins/backstage-plugin-${{ needs.split-tag.outputs.plugin }}/*.tgz
71+
files: plugins/${{ needs.split-tag.outputs.plugin }}/*.tgz

.github/workflows/test.yaml

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,22 @@ jobs:
2929
id: filter
3030
with:
3131
filters: |
32-
coder:
32+
backstage-plugin-coder:
3333
- ".github/workflows/test.yaml"
3434
- "yarn.lock"
3535
- "plugins/backstage-plugin-coder/**"
36-
devcontainers-backend:
36+
backstage-plugin-devcontainers-backend:
3737
- ".github/workflows/test.yaml"
3838
- "yarn.lock"
3939
- "plugins/backstage-plugin-devcontainers-backend/**"
40-
devcontainers-react:
40+
backstage-plugin-devcontainers-react:
4141
- ".github/workflows/test.yaml"
4242
- "yarn.lock"
4343
- "plugins/backstage-plugin-devcontainers-react/**"
44+
auth-backend-module-coder-provider:
45+
- ".github/workflows/test.yaml"
46+
- "yarn.lock"
47+
- "plugins/auth-backend-module-coder-provider/**"
4448
plugin:
4549
needs: changes
4650
if: ${{ needs.changes.outputs.plugins != '' && toJson(fromJson(needs.changes.outputs.plugins)) != '[]' }}
@@ -51,7 +55,7 @@ jobs:
5155
name: ${{ matrix.plugin }}
5256
defaults:
5357
run:
54-
working-directory: plugins/backstage-plugin-${{ matrix.plugin }}
58+
working-directory: plugins/${{ matrix.plugin }}
5559
steps:
5660
- uses: actions/checkout@v4
5761
- uses: actions/setup-node@v4
@@ -60,7 +64,7 @@ jobs:
6064
cache: yarn
6165
- run: yarn install --frozen-lockfile
6266
# The Prettier command is in the root package.json.
63-
- run: yarn prettier --check plugins/backstage-plugin-${{ matrix.plugin }}
67+
- run: yarn prettier --check plugins/${{ matrix.plugin }}
6468
id: fmt
6569
working-directory: .
6670
- run: yarn lint
@@ -73,20 +77,29 @@ jobs:
7377
- run: yarn test
7478
id: test
7579
if: success() || failure()
76-
# Must run tsc first to generate the .d.ts files.
77-
- run: yarn tsc && yarn build
80+
- run: yarn tsc
81+
working-directory: .
82+
if: success() || failure()
83+
# Build the plugin
84+
- run: yarn build
7885
id: build
86+
if: success() || failure()
7987
# Version it with the SHA and upload to the run as an artifact in case
8088
# someone needs to download it for testing.
81-
- run: yarn version --new-version "0.0.0-devel+$GITHUB_SHA"
89+
- name: Set development version
8290
id: version
83-
- run: yarn pack
91+
run: |
92+
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
93+
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='dev-${SHORT_SHA}'; fs.writeFileSync('package.json', JSON.stringify(pkg,null,2)+'\n');"
94+
echo "Set version to dev-${SHORT_SHA}"
95+
- name: Pack plugin
8496
id: pack
97+
run: yarn pack --out '%s-%v.tgz'
8598
- uses: actions/upload-artifact@v4
8699
id: upload
87100
with:
88101
name: ${{ matrix.plugin }}
89-
path: plugins/backstage-plugin-${{ matrix.plugin }}/*.tgz
102+
path: plugins/${{ matrix.plugin }}/*.tgz
90103
# Since we continued on failures above, fail now if there were errors. We
91104
# allow skipped jobs to pass, but not failed or cancelled jobs.
92105
- name: Check required

.gitignore

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,27 @@ logs
77
npm-debug.log*
88
yarn-debug.log*
99
yarn-error.log*
10-
lerna-debug.log*
1110

1211
# Coverage directory generated when running tests with coverage
1312
coverage
1413

1514
# Dependencies
1615
node_modules/
1716

18-
# Yarn 3 files
17+
# Yarn files
1918
.pnp.*
2019
.yarn/*
21-
!.yarn/patches
22-
!.yarn/plugins
20+
# This is skipped because we specify the Yarn version in the root package.json
2321
!.yarn/releases
22+
# Important that we re-include the plugins directory because we need Backstage's
23+
# official Yarn plugin to simplify managing dependencies for each Backstage
24+
# release. The package.json files are updated to include the special
25+
# "backstage:^" value for version numbers, which will break without the plugin
26+
!.yarn/plugins
27+
!.yarn/patches
2428
!.yarn/sdks
2529
!.yarn/versions
2630

27-
# Node version directives
28-
.nvmrc
29-
3031
# dotenv environment variables file
3132
.env
3233
.env.test
@@ -52,3 +53,8 @@ site
5253

5354
# E2E test reports
5455
e2e-test-report/
56+
57+
# Cache
58+
.cache/
59+
# Local configuration overrides (contains secrets)
60+
app-config.local.yaml

.node-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
18.19.0
1+
20.19.0

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
20.19.0

.prettierignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@ dist
22
dist-types
33
coverage
44
.vscode
5-
.coder.yaml
6-
app-config.local.yaml
5+
.coder.yaml
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* eslint-disable */
2+
//prettier-ignore
3+
module.exports = {
4+
name: "@yarnpkg/plugin-backstage",
5+
factory: function (require) {
6+
"use strict";var plugin=(()=>{var ee=Object.create;var R=Object.defineProperty;var re=Object.getOwnPropertyDescriptor;var te=Object.getOwnPropertyNames;var oe=Object.getPrototypeOf,ne=Object.prototype.hasOwnProperty;var f=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(r,t)=>(typeof require<"u"?require:r)[t]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var se=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports),ae=(e,r)=>{for(var t in r)R(e,t,{get:r[t],enumerable:!0})},M=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of te(r))!ne.call(e,o)&&o!==t&&R(e,o,{get:()=>r[o],enumerable:!(n=re(r,o))||n.enumerable});return e};var S=(e,r,t)=>(t=e!=null?ee(oe(e)):{},M(r||!e||!e.__esModule?R(t,"default",{value:e,enumerable:!0}):t,e)),ie=e=>M(R({},"__esModule",{value:!0}),e);var H=se((Ae,z)=>{"use strict";var $=class e extends Error{constructor(r){super(e._prepareSuperMessage(r)),Object.defineProperty(this,"name",{value:"NonError",configurable:!0,writable:!0}),Error.captureStackTrace&&Error.captureStackTrace(this,e)}static _prepareSuperMessage(r){try{return JSON.stringify(r)}catch{return String(r)}}},fe=[{property:"name",enumerable:!1},{property:"message",enumerable:!1},{property:"stack",enumerable:!1},{property:"code",enumerable:!0}],D=Symbol(".toJSON called"),le=e=>{e[D]=!0;let r=e.toJSON();return delete e[D],r},T=({from:e,seen:r,to_:t,forceEnumerable:n,maxDepth:o,depth:a})=>{let i=t||(Array.isArray(e)?[]:{});if(r.push(e),a>=o)return i;if(typeof e.toJSON=="function"&&e[D]!==!0)return le(e);for(let[s,p]of Object.entries(e)){if(typeof Buffer=="function"&&Buffer.isBuffer(p)){i[s]="[object Buffer]";continue}if(typeof p!="function"){if(!p||typeof p!="object"){i[s]=p;continue}if(!r.includes(e[s])){a++,i[s]=T({from:e[s],seen:r.slice(),forceEnumerable:n,maxDepth:o,depth:a});continue}i[s]="[Circular]"}}for(let{property:s,enumerable:p}of fe)typeof e[s]=="string"&&Object.defineProperty(i,s,{value:e[s],enumerable:n?!0:p,configurable:!0,writable:!0});return i},ge=(e,r={})=>{let{maxDepth:t=Number.POSITIVE_INFINITY}=r;return typeof e=="object"&&e!==null?T({from:e,seen:[],forceEnumerable:!0,maxDepth:t,depth:0}):typeof e=="function"?`[Function: ${e.name||"anonymous"}]`:e},me=(e,r={})=>{let{maxDepth:t=Number.POSITIVE_INFINITY}=r;if(e instanceof Error)return e;if(typeof e=="object"&&e!==null&&!Array.isArray(e)){let n=new Error;return T({from:e,seen:[],to_:n,maxDepth:t,depth:0}),n}return new $(e)};z.exports={serializeError:ge,deserializeError:me}});var ve={};ae(ve,{default:()=>xe});var P=f("@yarnpkg/core");var E=f("@yarnpkg/core");var U=S(f("assert")),q=f("semver"),v=f("@yarnpkg/fslib");var d=S(f("fs")),l=f("path");function _(e,r){let t=e;for(let n=0;n<1e3;n++){let o=(0,l.resolve)(t,"package.json");if(d.default.existsSync(o)&&r(o))return t;let i=(0,l.dirname)(t);if(i===t)return;t=i}throw new Error(`Iteration limit reached when searching for root package.json at ${e}`)}function ce(e){let r=_(e,()=>!0);if(!r)throw new Error(`No package.json found while searching for package root of ${e}`);return r}function pe(e){if(!d.default.existsSync((0,l.resolve)(e,"src")))throw new Error("Tried to access monorepo package root dir outside of Backstage repository");return(0,l.resolve)(e,"../..")}function O(e){let r=ce(e),t=d.default.realpathSync(process.cwd()).replace(/^[a-z]:/,s=>s.toLocaleUpperCase("en-US")),n="",o=()=>(n||(n=pe(r)),n),a="",i=()=>(a||(a=_(t,s=>{try{let p=d.default.readFileSync(s,"utf8");return!!JSON.parse(p).workspaces}catch(p){throw new Error(`Failed to parse package.json file while searching for root, ${p}`)}})??t),a);return{ownDir:r,get ownRoot(){return o()},targetDir:t,get targetRoot(){return i()},resolveOwn:(...s)=>(0,l.resolve)(r,...s),resolveOwnRoot:(...s)=>(0,l.resolve)(o(),...s),resolveTarget:(...s)=>(0,l.resolve)(t,...s),resolveTargetRoot:(...s)=>(0,l.resolve)(i(),...s)}}var B="backstage.json";function u(e){if(typeof e!="object"||e===null||Array.isArray(e))return!1;let r=e;return!(typeof r.name!="string"||r.name===""||typeof r.message!="string")}var J=S(H());function G(e){if(u(e)){let r=String(e);return r!=="[object Object]"?r:`${e.name}: ${e.message}`}return`unknown error '${e}'`}var x=class extends Error{cause;constructor(r,t){let n=r;if(t!==void 0){let o=G(t);n?n+=`; caused by ${o}`:n=`caused by ${o}`}super(n),Error.captureStackTrace?.(this,this.constructor),(!this.name||this.name==="Error")&&this.constructor.name!=="Error"&&(this.name=this.constructor.name),this.cause=u(t)?t:void 0}};var k=class extends x{constructor(r,t){super(r,t),this.name=u(t)?t.name:"Error"}};var K=e=>{let r=!1,t;return()=>(r||(t=e(),r=!0),t)};var y=f("@yarnpkg/fslib");var Y=()=>y.npath.toPortablePath(O(y.npath.fromPortablePath(y.ppath.cwd())).targetRoot);var h=K(()=>{let e=v.ppath.join(Y(),B),r=null;try{let t=v.xfs.readJsonSync(e).version;(0,U.default)(t!==void 0,"Version field is missing"),r=(0,q.valid)(t),(0,U.default)(r!==null,"Version exists but is not valid semver")}catch(t){throw new k("Valid version string not found in backstage.json",t)}return r});var w=f("@yarnpkg/core"),Q=f("@yarnpkg/fslib");var ue="https://versions.backstage.io",de="https://raw.githubusercontent.com/backstage/versions/main";function ke(e,r){return new Promise((t,n)=>{let o=setTimeout(()=>{r.aborted||t()},e);r.addEventListener("abort",()=>{clearTimeout(o),n(new Error("Aborted"))})})}async function ye(e,r,t){let n=new AbortController,o=new AbortController,a=e(n.signal).then(s=>(o.abort(),s)),i=ke(t,o.signal).then(()=>r(o.signal)).then(s=>(n.abort(),s));return Promise.any([a,i]).catch(()=>a)}async function A(e){let r=encodeURIComponent(e.version),t=e.fetch??fetch,n=e.versionsBaseUrl??ue,o=e.gitHubRawBaseUrl??de,a=await ye(i=>t(`${n}/v1/releases/${r}/manifest.json`,{signal:i}),i=>t(`${o}/v1/releases/${r}/manifest.json`,{signal:i}),500);if(a.status===404)throw new Error(`No release found for ${e.version} version`);if(a.status!==200)throw new Error(`Unexpected response status ${a.status} when fetching release from ${a.url}.`);return a.json()}var c="backstage:";var j=f("process"),m=async(e,r)=>{let t=w.structUtils.stringifyIdent(e),n=w.structUtils.parseRange(e.range);if(n.protocol!==c)throw new Error(`Unsupported version protocol in version range "${e.range}" for package ${t}`);if(n.selector!=="^")throw new Error(`Unexpected version selector "${n.selector}" for package ${t}`);let o=h(),a=j.env.BACKSTAGE_MANIFEST_FILE,s=(a?await Q.xfs.readJsonSync(a):await A({version:o,versionsBaseUrl:j.env.BACKSTAGE_VERSIONS_BASE_URL,fetch:async p=>{let F=await w.httpUtils.get(p,{configuration:r,jsonResponse:!0});return{status:200,url:p,json:()=>F}}})).packages.find(p=>p.name===t);if(!s)throw new Error(`Package ${t} not found in manifest for Backstage v${o}. This means the specified package is not included in this Backstage release. This may imply the package has been replaced with an alternative - please review the documentation for the package. If you need to continue using this package, it will be necessary to switch to manually managing its version.`);return s.version};var he=e=>E.structUtils.parseRange(e).protocol===c,we=(e,r,t)=>e!=="dependencies"?e:t.manifest.ensureDependencyMeta(E.structUtils.makeDescriptor(r,"unknown")).optional?"optionalDependencies":e,L=async(e,r)=>{for(let t of["dependencies","devDependencies"]){let n=Array.from(e.manifest.getForScope(t).values()).filter(o=>o.range.startsWith(c));for(let o of n){let a=E.structUtils.stringifyIdent(o);if(E.structUtils.parseRange(o.range).selector!=="^")throw new Error(`Unexpected version range "${o.range}" for dependency on "${a}"`);let s=we(t,o,e);r[s][a]=`^${await m(o,e.project.configuration)}`}}if(["dependencies","devDependencies","optionalDependencies"].some(t=>Object.values(r[t]??{}).some(he)))throw new Error(`Failed to replace all "backstage:" ranges in manifest for ${r.name}`)};var C=f("@yarnpkg/core");var V=async(e,r)=>{let t=C.structUtils.parseRange(e.range);if(t.protocol!==c)return e;if(t.selector!=="^")throw new Error(`Invalid backstage: version range found: ${e.range}`);return C.structUtils.bindDescriptor(e,{backstage:h(),npm:await m(e,r.configuration)})};var X=f("@yarnpkg/core");var N=async(e,r,t,n)=>{let o=X.structUtils.parseRange(t.range);if(t.scope==="backstage"&&o.protocol!==c){let a=t.range;try{t.range=`${c}^`,await m(t,e.project.configuration),console.info(`Setting ${t.scope}/${t.name} to ${c}^`)}catch{t.range=a}}};var Z=f("@yarnpkg/core");var I=async(e,r,t,n)=>{let o=Z.structUtils.parseRange(n.range);n.scope==="backstage"&&o.protocol!==c&&console.warn(`${n.name} should be set to "${c}^" instead of "${n.range}". Make sure this change is intentional and not a mistake.`)};var g=f("@yarnpkg/core"),W=f("@yarnpkg/plugin-npm");var b=class e{static protocol=c;supportsDescriptor=r=>r.range.startsWith(e.protocol);async getCandidates(r,t,n){let o=g.structUtils.parseRange(r.range).params?.npm;if(!o||Array.isArray(o))throw new Error(`Missing npm parameter on backstage: range "${r.range}"`);return new W.NpmSemverResolver().getCandidates(g.structUtils.makeDescriptor(r,`npm:^${o}`),t,n)}getResolutionDependencies(r){let t=g.structUtils.parseRange(r.range).params?.npm;if(!t)throw new Error(`Missing npm parameter on backstage: range "${r.range}".`);return{[g.structUtils.stringifyIdent(r)]:g.structUtils.makeDescriptor(r,`npm:^${t}`)}}async getSatisfying(r,t,n,o){let a=r,i=g.structUtils.parseRange(a.range);if(i.protocol===c){let s=i.params?.npm;a=g.structUtils.makeDescriptor(r,`npm:^${s}`)}return new W.NpmSemverResolver().getSatisfying(a,t,n,o)}bindDescriptor=r=>r;supportsLocator=()=>!1;shouldPersistResolution=()=>{throw new Error("Unreachable: BackstageNpmResolver should never persist resolution as it uses npm: protocol")};resolve=async()=>{throw new Error("Unreachable: BackstageNpmResolver should never resolve as it uses npm: protocol")}};var Ee="\x1B[31;1m",be="\x1B[0m";P.semverUtils.satisfiesWithPrereleases(P.YarnVersion,"^4.1.1")||(console.error(),console.error(`${Ee}Unsupported yarn version${be}: The Backstage yarn plugin only works with yarn ^4.1.1. Please upgrade yarn, or remove this plugin with "yarn plugin remove @yarnpkg/plugin-backstage".`),console.error());var Re={hooks:{afterWorkspaceDependencyAddition:N,afterWorkspaceDependencyReplacement:I,reduceDependency:V,beforeWorkspacePacking:L},resolvers:[b]},xe=Re;return ie(ve);})();
7+
return plugin;
8+
}
9+
};

0 commit comments

Comments
 (0)